/*
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C
Copyright 2018 New Vector Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2015 OpenMarket Ltd

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
 */

#define MXKROOMVIEWCONTROLLER_DEFAULT_TYPING_TIMEOUT_SEC 10
#define MXKROOMVIEWCONTROLLER_MESSAGES_TABLE_MINIMUM_HEIGHT 50

#import "MXKRoomViewController.h"

#import <MediaPlayer/MediaPlayer.h>

#import "MXKRoomBubbleTableViewCell.h"
#import "MXKSearchTableViewCell.h"
#import "MXKImageView.h"

#import "MXKRoomDataSourceManager.h"

#import "MXKRoomInputToolbarViewWithSimpleTextView.h"

#import "MXKConstants.h"

#import "MXKRoomBubbleCellData.h"

#import "MXKEncryptionKeysImportView.h"

#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"

#import "MXKPreviewViewController.h"

// Constant used to determine whether an event is visible at the bottom of the tableview, based on its visible height
static const CGFloat kCellVisibilityMinimumHeight = 8.0;

@interface MXKRoomViewController () <MXKPreviewViewControllerDelegate>
{
    /**
     YES once the view has appeared
     */
    BOOL hasAppearedOnce;
    
    /**
     YES if scrolling to bottom is in progress
     */
    BOOL isScrollingToBottom;
    
    /**
     Date of the last observed typing
     */
    NSDate *lastTypingDate;
    
    /**
     Local typing timout
     */
    NSTimer *typingTimer;
    
    /**
     YES when pagination is in progress.
     */
    BOOL isPaginationInProgress;
    
    /**
     The back pagination spinner view.
     */
    UIView* backPaginationActivityView;
    
    /**
     Store the height of the first bubble before back pagination.
     */
    CGFloat backPaginationSavedFirstBubbleHeight;
    
    /**
     Potential request in progress to join the selected room
     */
    MXHTTPOperation *joinRoomRequest;
    
    /**
     Text selection
     */
    NSString *selectedText;
    
    /**
     The class used to instantiate attachments viewer for image and video..
     */
    Class attachmentsViewerClass;
    
    /**
     The class used to display event details.
     */
    Class customEventDetailsViewClass;
    
    /**
     The reconnection animated view.
     */
    UIView* reconnectingView;
    
    /**
     The view to import e2e keys.
     */
    MXKEncryptionKeysImportView *importView;

    /**
     The latest server sync date
     */
    NSDate* latestServerSync;
    
    /**
     The restart the event connnection
     */
    BOOL restartConnection;
}

/**
 The eventId of the Attachment that was used to open the Attachments ViewController
 */
@property (nonatomic) NSString *openedAttachmentEventId;

/**
 The eventId of the Attachment from which the Attachments ViewController was closed
 */
@property (nonatomic) NSString *closedAttachmentEventId;

@property (nonatomic) UIImageView *openedAttachmentImageView;

/**
 Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room.
 */
@property (nonatomic, weak) id mxSessionWillLeaveRoomNotificationObserver;

/**
 Observe UIApplicationDidBecomeActiveNotification to refresh bubbles when app leaves the background state.
 */
@property (nonatomic, weak) id uiApplicationDidBecomeActiveNotificationObserver;

/**
 Observe UIMenuControllerDidHideMenuNotification to cancel text selection
 */
@property (nonatomic, weak) id uiMenuControllerDidHideMenuNotificationObserver;

/**
 The attachments viewer for image and video.
 */
@property (nonatomic, weak) MXKAttachmentsViewController *attachmentsViewer;

@end

@implementation MXKRoomViewController
@synthesize roomDataSource, titleView, inputToolbarView, activitiesView;

#pragma mark - Class methods

+ (UINib *)nib
{
    return [UINib nibWithNibName:NSStringFromClass([MXKRoomViewController class])
                          bundle:[NSBundle bundleForClass:[MXKRoomViewController class]]];
}

+ (instancetype)roomViewController
{
    return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKRoomViewController class])
                                          bundle:[NSBundle bundleForClass:[MXKRoomViewController class]]];
}

#pragma mark -

- (void)finalizeInit
{
    [super finalizeInit];
    
    // Scroll to bottom the bubble history at first display
    shouldScrollToBottomOnTableRefresh = YES;
    
    // Default pagination settings
    _paginationThreshold = 300;
    _paginationLimit = 30;
    
    // Save progress text input by default
    _saveProgressTextInput = YES;
    
    // Enable auto join option by default
    _autoJoinInvitedRoom = YES;
    
    // Do not take ownership of room data source by default
    _hasRoomDataSourceOwnership = NO;
    
    // Turn on the automatic events acknowledgement.
    _eventsAcknowledgementEnabled = YES;
    
    // Do not update the read marker by default.
    _updateRoomReadMarker = NO;
    
    // Center the table content on the initial event top by default.
    _centerBubblesTableViewContentOnTheInitialEventBottom = NO;
    
    // Scroll to the bottom when a keyboard is presented
    _scrollHistoryToTheBottomOnKeyboardPresentation = YES;
    
    // Keep visible the status bar by default.
    isStatusBarHidden = NO;
    
    // By default actions button is shown in document preview
    _allowActionsInDocumentPreview = YES;
    
    // By default the duration of the composer resizing is 0.3s
    _resizeComposerAnimationDuration = 0.3;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    if (BuildSettings.newAppLayoutEnabled)
    {
        [self vc_setLargeTitleDisplayMode: UINavigationItemLargeTitleDisplayModeNever];
    }
    
    // Check whether the view controller has been pushed via storyboard
    if (!_bubblesTableView)
    {
        // Instantiate view controller objects
        [[[self class] nib] instantiateWithOwner:self options:nil];
    }
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated"
    // Adjust bottom constraint of the input toolbar container in order to take into account potential tabBar
    _roomInputToolbarContainerBottomConstraint.active = NO;
    _roomInputToolbarContainerBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide
                                                                              attribute:NSLayoutAttributeTop
                                                                              relatedBy:NSLayoutRelationEqual
                                                                                 toItem:self.roomInputToolbarContainer
                                                                              attribute:NSLayoutAttributeBottom
                                                                             multiplier:1.0f
                                                                               constant:0.0f];
    #pragma clang diagnostic pop
    
    _roomInputToolbarContainerBottomConstraint.active = YES;
    [self.view setNeedsUpdateConstraints];
    
    // Hide bubbles table by default in order to hide initial scrolling to the bottom
    _bubblesTableView.hidden = YES;
    
    // Ensure that the titleView will be scaled when it will be required
    // during a screen rotation for example.
    _roomTitleViewContainer.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    
    // Set default input toolbar view
    [self setRoomInputToolbarViewClass:MXKRoomInputToolbarViewWithSimpleTextView.class];
    
    // set the default extra
    [self setRoomActivitiesViewClass:MXKRoomActivitiesView.class];
    
    // Finalize table view configuration
    [self configureBubblesTableView];
    
    // Observe UIApplicationDidBecomeActiveNotification to refresh bubbles when app leaves the background state.
    MXWeakify(self);
    _uiApplicationDidBecomeActiveNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        if (self->roomDataSource.state == MXKDataSourceStateReady && [self->roomDataSource tableView:self->_bubblesTableView numberOfRowsInSection:0])
        {
            // Reload the full table
            self.bubbleTableViewDisplayInTransition = YES;
            [self reloadBubblesTable:YES];
            self.bubbleTableViewDisplayInTransition = NO;
        }
    }];
    
    if ([MXKAppSettings standardAppSettings].outboundGroupSessionKeyPreSharingStrategy == MXKKeyPreSharingWhenEnteringRoom)
    {
        [self shareEncryptionKeys];
    }
}

- (BOOL)prefersStatusBarHidden
{
    // Return the current status bar visibility.
    // Caution: Enable [UIViewController prefersStatusBarHidden] use at application level
    // by turning on UIViewControllerBasedStatusBarAppearance in Info.plist.
    return isStatusBarHidden;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    [self.navigationController setToolbarHidden:YES animated:NO];
    
    // Observe server sync process at room data source level too
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionChange) name:kMXKRoomDataSourceSyncStatusChanged object:nil];

    // Observe timeline failure
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTimelineError:) name:kMXKRoomDataSourceTimelineError object:nil];

    // Observe the server sync
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSyncNotification) name:kMXSessionDidSyncNotification object:nil];
    
    // Be sure to display the activity indicator during back pagination
    if (isPaginationInProgress)
    {
        [self startActivityIndicator];
    }
    
    // Finalize view controller appearance
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateViewControllerAppearanceOnRoomDataSourceState];
    });
    
    // no need to reload the tableview at this stage
    // IOS is going to load it after calling this method
    // so give a breath to scroll to the bottom if required
    if (shouldScrollToBottomOnTableRefresh)
    {
        self.bubbleTableViewDisplayInTransition = YES;
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            [self scrollBubblesTableViewToBottomAnimated:NO];
            
            // Show bubbles table after initial scrolling to the bottom
            // Patch: We need to delay this operation to wait for the end of scrolling.
            dispatch_after(dispatch_walltime(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
                
                self->_bubblesTableView.hidden = NO;
                self.bubbleTableViewDisplayInTransition = NO;
                
            });
            
        });
    }
    else
    {
        _bubblesTableView.hidden = NO;
    }
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    // Remove the rounded bottom unsafe area of the iPhone X
    _bubblesTableViewBottomConstraint.constant += self.view.safeAreaInsets.bottom;

    if (_saveProgressTextInput && roomDataSource)
    {
        // Retrieve the potential message partially typed during last room display.
        // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early)
        [inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage];
    }
    
    if (!hasAppearedOnce)
    {
        hasAppearedOnce = YES;
    }
    
    //  Mark all messages as read when the room is displayed
    [self.roomDataSource.room.summary markAllAsReadLocally];
    
    [self updateCurrentEventIdAtTableBottom:YES];
    
    if (!self.isContextPreview)
    {
        [self.roomDataSource.room resetUnread];
    }
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKRoomDataSourceSyncStatusChanged object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKRoomDataSourceTimelineError object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidSyncNotification object:nil];
    
    [self removeReconnectingView];
}

- (void)dealloc
{
    if (_mxSessionWillLeaveRoomNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:_mxSessionWillLeaveRoomNotificationObserver];
    }
    
    if (_uiApplicationDidBecomeActiveNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:_uiApplicationDidBecomeActiveNotificationObserver];
    }
    
    if (_uiMenuControllerDidHideMenuNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:_uiMenuControllerDidHideMenuNotificationObserver];
    }
    
    [self destroy];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    
    // Dispose of any resources that can be recreated.
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
    isSizeTransitionInProgress = YES;
    shouldScrollToBottomOnTableRefresh = [self isBubblesTableScrollViewAtTheBottom];
    
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(coordinator.transitionDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        if (!self.keyboardView)
        {
            [self updateMessageTextViewFrame];
        }
        
        // Force full table refresh to take into account cell width change.
        self.bubbleTableViewDisplayInTransition = YES;
        [self reloadBubblesTable:YES invalidateBubblesCellDataCache:YES];
        self.bubbleTableViewDisplayInTransition = NO;
        
        self->shouldScrollToBottomOnTableRefresh = NO;
        self->isSizeTransitionInProgress = NO;
    });
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
// The 2 following methods are deprecated since iOS 8
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    isSizeTransitionInProgress = YES;
    shouldScrollToBottomOnTableRefresh = [self isBubblesTableScrollViewAtTheBottom];
    
    [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
    [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
    
    if (!self.keyboardView)
    {
        [self updateMessageTextViewFrame];
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        // Force full table refresh to take into account cell width change.
        self.bubbleTableViewDisplayInTransition = YES;
        [self reloadBubblesTable:YES];
        self.bubbleTableViewDisplayInTransition = NO;
        
        self->shouldScrollToBottomOnTableRefresh = NO;
        self->isSizeTransitionInProgress = NO;
    });
}
#pragma clang diagnostic pop

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    
    CGFloat bubblesTableViewBottomConst = self.roomInputToolbarContainerBottomConstraint.constant + self.roomInputToolbarContainerHeightConstraint.constant + self.roomActivitiesContainerHeightConstraint.constant;

    if (self.bubblesTableViewBottomConstraint.constant != bubblesTableViewBottomConst)
    {
        self.bubblesTableViewBottomConstraint.constant = bubblesTableViewBottomConst;
    }

}

#pragma mark - Override MXKViewController

- (void)onMatrixSessionChange
{
    [super onMatrixSessionChange];
    
    // Check dataSource state
    if (self.roomDataSource && (self.roomDataSource.state == MXKDataSourceStatePreparing || self.roomDataSource.serverSyncEventCount))
    {
        // dataSource is not ready, keep running the loading wheel
        [self startActivityIndicator];
    }
}

- (void)onKeyboardShowAnimationComplete
{
    // Check first if the first responder belongs to title view
    UIView *keyboardView = titleView.inputAccessoryView.superview;
    if (!keyboardView)
    {
        // Check whether the first responder is the input tool bar text composer
        keyboardView = inputToolbarView.inputAccessoryViewForKeyboard.superview;
    }
    
    // Report the keyboard view in order to track keyboard frame changes
    self.keyboardView = keyboardView;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
{
    // Deduce the bottom constraint for the input toolbar view (Don't forget the potential tabBar)
    CGFloat inputToolbarViewBottomConst = keyboardHeight - self.bottomLayoutGuide.length;
    // Check whether the keyboard is over the tabBar
    if (inputToolbarViewBottomConst < 0)
    {
        inputToolbarViewBottomConst = 0;
    }
    
    // Update constraints
    _roomInputToolbarContainerBottomConstraint.constant = inputToolbarViewBottomConst;
    _bubblesTableViewBottomConstraint.constant = inputToolbarViewBottomConst + _roomInputToolbarContainerHeightConstraint.constant + _roomActivitiesContainerHeightConstraint.constant;
    
    // Remove the rounded bottom unsafe area of the iPhone X
    _bubblesTableViewBottomConstraint.constant += self.view.safeAreaInsets.bottom;

    // Invalidate the current layout to take into account the new constraints in the next update cycle.
    [self.view setNeedsLayout];
    
    // Compute the visible area (tableview + toolbar) at the end of animation
    CGFloat visibleArea = self.view.frame.size.height - _bubblesTableView.adjustedContentInset.top - keyboardHeight;
    // Deduce max height of the message text input by considering the minimum height of the table view.
    inputToolbarView.maxHeight = visibleArea - MXKROOMVIEWCONTROLLER_MESSAGES_TABLE_MINIMUM_HEIGHT;
    
    // Check conditions before scrolling the tableview content when a new keyboard is presented.
    if ((_scrollHistoryToTheBottomOnKeyboardPresentation || [self isBubblesTableScrollViewAtTheBottom]) && !super.keyboardHeight && keyboardHeight && !currentAlert)
    {
        self.bubbleTableViewDisplayInTransition = YES;
        
        // Force here the layout update to scroll correctly the table content.
        [self.view layoutIfNeeded];
        [self scrollBubblesTableViewToBottomAnimated:NO];
        
        self.bubbleTableViewDisplayInTransition = NO;
    }
    else
    {
        [self updateCurrentEventIdAtTableBottom:NO];
    }
    
    super.keyboardHeight = keyboardHeight;
}
#pragma clang diagnostic pop

- (void)destroy
{
    if (documentInteractionController)
    {
        [documentInteractionController dismissPreviewAnimated:NO];
        [documentInteractionController dismissMenuAnimated:NO];
        documentInteractionController = nil;
    }
    
    if (currentSharedAttachment)
    {
        [currentSharedAttachment onShareEnded];
        currentSharedAttachment = nil;
    }
    
    [self dismissTemporarySubViews];
    
    _bubblesTableView.dataSource = nil;
    _bubblesTableView.delegate = nil;
    _bubblesTableView = nil;
    
    if (roomDataSource.delegate == self)
    {
        roomDataSource.delegate = nil;
    }
    
    if (_hasRoomDataSourceOwnership)
    {
        // Release the room data source
        [roomDataSource destroy];
    }
    roomDataSource = nil;
    
    if (titleView)
    {
        [titleView removeFromSuperview];
        [titleView destroy];
        titleView = nil;
    }
    
    if (inputToolbarView)
    {
        [inputToolbarView removeFromSuperview];
        [inputToolbarView destroy];
        inputToolbarView = nil;
    }
    
    if (activitiesView)
    {
        [activitiesView removeFromSuperview];
        [activitiesView destroy];
        activitiesView = nil;
    }
    
    [typingTimer invalidate];
    typingTimer = nil;
    
    if (joinRoomRequest)
    {
        [joinRoomRequest cancel];
        joinRoomRequest = nil;
    }

    [super destroy];
}

#pragma mark -

- (void)configureBubblesTableView
{
    // Set up table delegates
    _bubblesTableView.delegate = self;
    _bubblesTableView.dataSource = roomDataSource; // Note: data source may be nil here, it will be set during [displayRoom:] call.
    
    // Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room.
    MXWeakify(self);
    _mxSessionWillLeaveRoomNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionWillLeaveRoomNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
        
        MXStrongifyAndReturnIfNil(self);
        // Check whether the user will leave the current room
        if (notif.object == self.mainSession)
        {
            NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey];
            if (roomId && [roomId isEqualToString:self->roomDataSource.roomId])
            {
                // Update view controller appearance
                [self leaveRoomOnEvent:notif.userInfo[kMXSessionNotificationEventKey]];
            }
        }
    }];
}

- (void)updateMessageTextViewFrame
{
    if (!self.keyboardView)
    {
        // Compute the visible area (tableview + toolbar)
        CGFloat visibleArea = self.view.frame.size.height - _bubblesTableView.adjustedContentInset.top;
        // Deduce max height of the message text input by considering the minimum height of the table view.
        inputToolbarView.maxHeight = visibleArea - MXKROOMVIEWCONTROLLER_MESSAGES_TABLE_MINIMUM_HEIGHT;
    }
}

- (CGFloat)tableViewSafeAreaWidth
{
    CGFloat safeAreaInsetsWidth;
    
    // Take safe area into account
    safeAreaInsetsWidth = self.bubblesTableView.safeAreaInsets.left + self.bubblesTableView.safeAreaInsets.right;
    
    return self.bubblesTableView.frame.size.width - safeAreaInsetsWidth;
}

#pragma mark - Public API

- (void)displayRoom:(MXKRoomDataSource *)dataSource
{
    if (roomDataSource)
    {
        if (self.hasRoomDataSourceOwnership)
        {
            // Release the room data source
            [roomDataSource destroy];
        }
        else if (roomDataSource.delegate == self)
        {
            roomDataSource.delegate = nil;
        }
        roomDataSource = nil;
        
        [self removeMatrixSession:self.mainSession];
    }
    
    // Reset the current event id
    currentEventIdAtTableBottom = nil;
    
    if (dataSource)
    {
        if (dataSource.isPeeking)
        {
            // Remove the input toolbar in case of peeking.
            // We do not let the user type message in this case.
            [self setRoomInputToolbarViewClass:nil];
        }
        
        roomDataSource = dataSource;
        roomDataSource.delegate = self;
        roomDataSource.paginationLimitAroundInitialEvent = _paginationLimit;
        
        // Report the matrix session at view controller level to update UI according to session state
        [self addMatrixSession:roomDataSource.mxSession];
        
        if (_bubblesTableView)
        {
            [self dismissTemporarySubViews];
            
            // Set up table data source
            _bubblesTableView.dataSource = roomDataSource;
        }
        
        // When ready, do the initial back pagination
        if (roomDataSource.state == MXKDataSourceStateReady)
        {
            [self onRoomDataSourceReady];
        }
    }
    
    [self updateViewControllerAppearanceOnRoomDataSourceState];
}

- (void)onRoomDataSourceReady
{
    // If the user is only invited, auto-join the room if this option is enabled
    if (roomDataSource.room.summary.membership == MXMembershipInvite)
    {
        if (_autoJoinInvitedRoom)
        {
            [self joinRoom:nil];
        }
    }
    else
    {
        [self triggerInitialBackPagination];
    }
}

- (void)updateViewControllerAppearanceOnRoomDataSourceState
{
    // Update UI by considering dataSource state
    if (roomDataSource && roomDataSource.state == MXKDataSourceStateReady)
    {
        [self stopActivityIndicator];
        
        if (titleView)
        {
            titleView.mxRoom = roomDataSource.room;
            titleView.editable = YES;
            titleView.hidden = NO;
        }
        else
        {
            // set default title
            self.navigationItem.title = roomDataSource.room.summary.displayName;
        }
        
        // Show input tool bar
        inputToolbarView.hidden = NO;
    }
    else
    {
        // Update the title except if the room has just been left
        if (!_leftRoomReasonLabel)
        {
            if (roomDataSource && roomDataSource.state == MXKDataSourceStatePreparing)
            {
                if (titleView)
                {
                    titleView.mxRoom = roomDataSource.room;
                    titleView.hidden = (!titleView.mxRoom);
                }
                else
                {
                    self.navigationItem.title = roomDataSource.room.summary.displayName;
                }
            }
            else
            {
                if (titleView)
                {
                    titleView.mxRoom = nil;
                    titleView.hidden = NO;
                }
                else
                {
                    self.navigationItem.title = nil;
                }
            }
        }
        titleView.editable = NO;
        
        // Hide input tool bar
        inputToolbarView.hidden = YES;
    }
    
    // Finalize room title refresh
    [titleView refreshDisplay];
    
    if (activitiesView)
    {
        // Hide by default the activity view when no room is displayed
        activitiesView.hidden = (roomDataSource == nil);
    }
}

- (void)onTimelineError:(NSNotification *)notif
{
    if (notif.object == roomDataSource)
    {
        [self stopActivityIndicator];

        // Compute the message to display to the end user
        NSString *errorTitle;
        NSString *errorMessage;

        NSError *error = notif.userInfo[kMXKRoomDataSourceTimelineErrorErrorKey];
        if ([MXError isMXError:error])
        {
            MXError *mxError = [[MXError alloc] initWithNSError:error];
            if ([mxError.errcode isEqualToString:kMXErrCodeStringNotFound])
            {
                errorTitle = [VectorL10n roomErrorTimelineEventNotFoundTitle];
                errorMessage = [VectorL10n roomErrorTimelineEventNotFound];
            }
            else
            {
                errorTitle = [VectorL10n roomErrorCannotLoadTimeline];
                errorMessage = mxError.error;
            }
        }
        else
        {
            errorTitle = [VectorL10n roomErrorCannotLoadTimeline];
        }

        // And show it
        [currentAlert dismissViewControllerAnimated:NO completion:nil];

        __weak typeof(self) weakSelf = self;
        UIAlertController *errorAlert = [UIAlertController alertControllerWithTitle:errorTitle
                                                                            message:errorMessage
                                                                     preferredStyle:UIAlertControllerStyleAlert];
        
        [errorAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
                                                       style:UIAlertActionStyleDefault
                                                     handler:^(UIAlertAction * action) {
                                                    
                                                    typeof(self) self = weakSelf;
                                                    self->currentAlert = nil;
                                                    
                                                }]];

        [self presentViewController:errorAlert animated:YES completion:nil];
        currentAlert = errorAlert;
    }
}

- (void)joinRoom:(void(^)(MXKRoomViewControllerJoinRoomResult result))completion
{
    if (joinRoomRequest != nil)
    {
        if (completion)
        {
            completion(MXKRoomViewControllerJoinRoomResultFailureJoinInProgress);
        }
        return;
    }
    
    UserIndicatorCancel cancelIndicator = [self.userIndicatorStore presentLoadingWithLabel:[VectorL10n joining] isInteractionBlocking:YES];
    joinRoomRequest = [roomDataSource.room join:^{
        
        self->joinRoomRequest = nil;
        cancelIndicator();
        
        [self triggerInitialBackPagination];
        
        if (completion)
        {
            completion(MXKRoomViewControllerJoinRoomResultSuccess);
        }
        
    } failure:^(NSError *error) {
        cancelIndicator();
        MXLogDebug(@"[MXKRoomVC] Failed to join room (%@)", self->roomDataSource.room.summary.displayName);
        [self processRoomJoinFailureWithError:error completion:completion];
    }];
}

- (void)joinRoomWithRoomIdOrAlias:(NSString*)roomIdOrAlias
                       viaServers:(NSArray<NSString*>*)viaServers
                       andSignUrl:(NSString*)signUrl
                       completion:(void(^)(MXKRoomViewControllerJoinRoomResult result))completion
{
    if (joinRoomRequest != nil)
    {
        if (completion)
        {
            completion(MXKRoomViewControllerJoinRoomResultFailureJoinInProgress);
        }
        
        return;
    }
    
    UserIndicatorCancel cancelIndicator = [self.userIndicatorStore presentLoadingWithLabel:[VectorL10n joining] isInteractionBlocking:YES];
    void (^success)(MXRoom *room) = ^(MXRoom *room) {
        
        self->joinRoomRequest = nil;
        cancelIndicator();
        
        MXWeakify(self);
        
        // The room is now part of the user's room
        MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession];
        
        [roomDataSourceManager roomDataSourceForRoom:room.roomId create:YES onComplete:^(MXKRoomDataSource *newRoomDataSource) {
            
            MXStrongifyAndReturnIfNil(self);
            
            // And can be displayed
            [self displayRoom:newRoomDataSource];
            
            if (completion)
            {
                completion(MXKRoomViewControllerJoinRoomResultSuccess);
            }
        }];
    };
    
    void (^failure)(NSError *error) = ^(NSError *error) {
        cancelIndicator();
        MXLogDebug(@"[MXKRoomVC] Failed to join room (%@)", roomIdOrAlias);
        [self processRoomJoinFailureWithError:error completion:completion];
    };
    
    // Does the join need to be validated before?
    if (signUrl)
    {
        joinRoomRequest = [self.mainSession joinRoom:roomIdOrAlias viaServers:viaServers withSignUrl:signUrl success:success failure:failure];
    }
    else
    {
        joinRoomRequest = [self.mainSession joinRoom:roomIdOrAlias viaServers:viaServers success:success failure:failure];
    }
}

- (void)processRoomJoinFailureWithError:(NSError *)error completion:(void(^)(MXKRoomViewControllerJoinRoomResult result))completion
{
    self->joinRoomRequest = nil;
    [self stopActivityIndicator];
    
    // Show the error to the end user
    NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
    
    // FIXME: We should hide this inside the SDK and expose it as a domain specific error
    BOOL isRoomEmpty = [msg isEqualToString:@"No known servers"];
    if (isRoomEmpty)
    {
        // minging kludge until https://matrix.org/jira/browse/SYN-678 is fixed
        // 'Error when trying to join an empty room should be more explicit'
        msg = [VectorL10n roomErrorJoinFailedEmptyRoom];
    }
    
    MXWeakify(self);
    [self->currentAlert dismissViewControllerAnimated:NO completion:nil];
    
    UIAlertController *errorAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomErrorJoinFailedTitle]
                                                                        message:msg
                                                                 preferredStyle:UIAlertControllerStyleAlert];
    
    [errorAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
                                                   style:UIAlertActionStyleDefault
                                                 handler:^(UIAlertAction * action) {
        
        MXStrongifyAndReturnIfNil(self);
        self->currentAlert = nil;
        
        if (completion)
        {
            completion((isRoomEmpty ? MXKRoomViewControllerJoinRoomResultFailureRoomEmpty : MXKRoomViewControllerJoinRoomResultFailureGeneric));
        }
    }]];
    
    [self presentViewController:errorAlert animated:YES completion:nil];
    currentAlert = errorAlert;
}

- (void)leaveRoomOnEvent:(MXEvent*)event
{
    [self dismissTemporarySubViews];
    
    NSString *reason = nil;
    if (event)
    {
        MXKEventFormatterError error;
        reason = [roomDataSource.eventFormatter
                  stringFromEvent:event
                  withRoomState:roomDataSource.roomState
                  andLatestRoomState:nil
                  error:&error];
        if (error != MXKEventFormatterErrorNone)
        {
            reason = nil;
        }
    }
    
    if (!reason.length)
    {
        if (self.roomDataSource.room.isDirect)
        {
            reason = [VectorL10n roomLeftForDm];
        }
        else
        {
            reason = [VectorL10n roomLeft];
        }
    }
    
    
    _bubblesTableView.dataSource = nil;
    _bubblesTableView.delegate = nil;
    
    if (self.hasRoomDataSourceOwnership)
    {
        // Release the room data source
        [roomDataSource destroy];
    }
    else if (roomDataSource.delegate == self)
    {
        roomDataSource.delegate = nil;
    }
    roomDataSource = nil;
    
    // Add reason label
    UILabel *leftRoomReasonLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, self.view.frame.size.width - 20, 70)];
    leftRoomReasonLabel.numberOfLines = 0;
    leftRoomReasonLabel.text = reason;
    leftRoomReasonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    _bubblesTableView.tableHeaderView = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 80)];
    [_bubblesTableView.tableHeaderView addSubview:leftRoomReasonLabel];
    [_bubblesTableView reloadData];
    
    _leftRoomReasonLabel = leftRoomReasonLabel;
    
    [self updateViewControllerAppearanceOnRoomDataSourceState];
}

- (void)setPaginationLimit:(NSUInteger)paginationLimit
{
    _paginationLimit = paginationLimit;

    // Use the same value when loading messages around the initial event
    roomDataSource.paginationLimitAroundInitialEvent = _paginationLimit;
}

- (void)setRoomTitleViewClass:(Class)roomTitleViewClass
{
    if ([self.titleView.class isEqual:roomTitleViewClass]) {
        return;
    }
    
    // Sanity check: accept only MXKRoomTitleView classes or sub-classes
    NSParameterAssert([roomTitleViewClass isSubclassOfClass:MXKRoomTitleView.class]);
    
    // Remove potential title view
    if (titleView)
    {
        [NSLayoutConstraint deactivateConstraints:titleView.constraints];
        
        [titleView dismissKeyboard];
        [titleView removeFromSuperview];
        [titleView destroy];
    }
    
    self.navigationItem.titleView = titleView = [roomTitleViewClass roomTitleView];
    titleView.delegate = self;
    
    // Define directly the navigation titleView with the custom title view instance. Do not use anymore a container.
    self.navigationItem.titleView = titleView;
    
    [self updateViewControllerAppearanceOnRoomDataSourceState];
}

- (void)setRoomInputToolbarViewClass:(Class)roomInputToolbarViewClass
{
    if (!_roomInputToolbarContainer)
    {
        MXLogDebug(@"[MXKRoomVC] Set roomInputToolbarViewClass failed: container is missing");
        return;
    }

    // Remove potential toolbar
    if (inputToolbarView)
    {
        MXLogDebug(@"[MXKRoomVC] setRoomInputToolbarViewClass: Set inputToolbarView with class %@ to nil", [self.inputToolbarView class]);
        
        [NSLayoutConstraint deactivateConstraints:inputToolbarView.constraints];
        [inputToolbarView dismissKeyboard];
        [inputToolbarView removeFromSuperview];
        [inputToolbarView destroy];
        inputToolbarView = nil;
    }
    
    if (roomDataSource && roomDataSource.isPeeking)
    {
        // Do not show the input toolbar if the displayed timeline in case of peeking.
        // We do not let the user type message in this case.
        roomInputToolbarViewClass = nil;
    }
    
    if (roomInputToolbarViewClass)
    {
        // Sanity check: accept only MXKRoomInputToolbarView classes or sub-classes
        NSParameterAssert([roomInputToolbarViewClass isSubclassOfClass:MXKRoomInputToolbarView.class]);
        
        MXLogDebug(@"[MXKRoomVC] setRoomInputToolbarViewClass: Set inputToolbarView to class %@", roomInputToolbarViewClass);
        
        id inputToolbarView = [roomInputToolbarViewClass instantiateRoomInputToolbarView];
        self->inputToolbarView = inputToolbarView;
        self->inputToolbarView.delegate = self;
        
        // Add the input toolbar view and define edge constraints
        [_roomInputToolbarContainer addSubview:inputToolbarView];
        [_roomInputToolbarContainer addConstraint:[NSLayoutConstraint constraintWithItem:_roomInputToolbarContainer
                                                                               attribute:NSLayoutAttributeBottom
                                                                               relatedBy:NSLayoutRelationEqual
                                                                                  toItem:inputToolbarView
                                                                               attribute:NSLayoutAttributeBottom
                                                                              multiplier:1.0f
                                                                                constant:0.0f]];
        [_roomInputToolbarContainer addConstraint:[NSLayoutConstraint constraintWithItem:_roomInputToolbarContainer
                                                                               attribute:NSLayoutAttributeTop
                                                                               relatedBy:NSLayoutRelationEqual
                                                                                  toItem:inputToolbarView
                                                                               attribute:NSLayoutAttributeTop
                                                                              multiplier:1.0f
                                                                                constant:0.0f]];
        [_roomInputToolbarContainer addConstraint:[NSLayoutConstraint constraintWithItem:_roomInputToolbarContainer
                                                                               attribute:NSLayoutAttributeLeading
                                                                               relatedBy:NSLayoutRelationEqual
                                                                                  toItem:inputToolbarView
                                                                               attribute:NSLayoutAttributeLeading
                                                                              multiplier:1.0f
                                                                                constant:0.0f]];
        [_roomInputToolbarContainer addConstraint:[NSLayoutConstraint constraintWithItem:_roomInputToolbarContainer
                                                                               attribute:NSLayoutAttributeTrailing
                                                                               relatedBy:NSLayoutRelationEqual
                                                                                  toItem:inputToolbarView
                                                                               attribute:NSLayoutAttributeTrailing
                                                                              multiplier:1.0f
                                                                                constant:0.0f]];
    }
    
    [_roomInputToolbarContainer setNeedsUpdateConstraints];
}


- (void)setRoomActivitiesViewClass:(Class)roomActivitiesViewClass
{
    if (!_roomActivitiesContainer)
    {
        MXLogDebug(@"[MXKRoomVC] Set RoomActivitiesViewClass failed: container is missing");
        return;
    }

    // Remove potential toolbar
    if (activitiesView)
    {
        [NSLayoutConstraint deactivateConstraints:activitiesView.constraints];
        [activitiesView removeFromSuperview];
        [activitiesView destroy];
        activitiesView = nil;
    }
    
    if (roomActivitiesViewClass)
    {
        // Sanity check: accept only MXKRoomExtraInfoView classes or sub-classes
        NSParameterAssert([roomActivitiesViewClass isSubclassOfClass:MXKRoomActivitiesView.class]);
        
        activitiesView = [roomActivitiesViewClass roomActivitiesView];
        
        // Add the view and define edge constraints
        activitiesView.translatesAutoresizingMaskIntoConstraints = NO;
        [_roomActivitiesContainer addSubview:activitiesView];
        
        NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:_roomActivitiesContainer
                                                                         attribute:NSLayoutAttributeTop
                                                                         relatedBy:NSLayoutRelationEqual
                                                                            toItem:activitiesView
                                                                         attribute:NSLayoutAttributeTop
                                                                        multiplier:1.0f
                                                                          constant:0.0f];
        
        
        NSLayoutConstraint* leadingConstraint = [NSLayoutConstraint constraintWithItem:_roomActivitiesContainer
                                                                             attribute:NSLayoutAttributeLeading
                                                                             relatedBy:NSLayoutRelationEqual
                                                                                toItem:activitiesView
                                                                             attribute:NSLayoutAttributeLeading
                                                                            multiplier:1.0f
                                                                              constant:0.0f];
        
        NSLayoutConstraint* widthConstraint = [NSLayoutConstraint constraintWithItem:_roomActivitiesContainer
                                                                           attribute:NSLayoutAttributeWidth
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:activitiesView
                                                                           attribute:NSLayoutAttributeWidth
                                                                          multiplier:1.0f
                                                                            constant:0.0f];
        
        NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:_roomActivitiesContainer
                                                                            attribute:NSLayoutAttributeHeight
                                                                            relatedBy:NSLayoutRelationEqual
                                                                               toItem:activitiesView
                                                                            attribute:NSLayoutAttributeHeight
                                                                           multiplier:1.0f
                                                                             constant:0.0f];
        

        [NSLayoutConstraint activateConstraints:@[topConstraint, leadingConstraint, widthConstraint, heightConstraint]];
        
        // let the provide view to define a height.
        // it could have no constrainst if there is no defined xib
        _roomActivitiesContainerHeightConstraint.constant = activitiesView.height;

        // Listen to activities view change
        activitiesView.delegate = self;
    }
    else
    {
        _roomActivitiesContainerHeightConstraint.constant = 0;
    }
    
    _bubblesTableViewBottomConstraint.constant = _roomInputToolbarContainerBottomConstraint.constant + _roomInputToolbarContainerHeightConstraint.constant +_roomActivitiesContainerHeightConstraint.constant;

    [_roomActivitiesContainer setNeedsUpdateConstraints];
}

- (void)setAttachmentsViewerClass:(Class)theAttachmentsViewerClass
{
    if (theAttachmentsViewerClass)
    {
        // Sanity check: accept only MXKAttachmentsViewController classes or sub-classes
        NSParameterAssert([theAttachmentsViewerClass isSubclassOfClass:MXKAttachmentsViewController.class]);
    }
    
    attachmentsViewerClass = theAttachmentsViewerClass;
}

- (void)setEventDetailsViewClass:(Class)eventDetailsViewClass
{
    if (eventDetailsViewClass)
    {
        // Sanity check: accept only MXKEventDetailsView classes or sub-classes
        NSParameterAssert([eventDetailsViewClass isSubclassOfClass:MXKEventDetailsView.class]);
    }
    
    customEventDetailsViewClass = eventDetailsViewClass;
}

- (BOOL)sendAsIRCStyleCommandIfPossible:(NSString*)string
{
    // Check whether the provided text may be an IRC-style command
    if ([string hasPrefix:@"/"] == NO || [string hasPrefix:@"//"] == YES)
    {
        return NO;
    }
    
    // Parse command line
    NSArray *components = [string componentsSeparatedByString:@" "];
    NSString *cmd = [components objectAtIndex:0];
    NSUInteger index = 1;
    
    // TODO: display an alert with the cmd usage in case of error or unrecognized cmd.
    NSString *cmdUsage;

    NSString* kMXKSlashCmdChangeDisplayName = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeDisplayName];
    NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom];
    NSString* kMXKSlashCmdPartRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandPartRoom];
    NSString* kMXKSlashCmdChangeRoomTopic = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeRoomTopic];

    
    if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]])
    {
        // send message as an emote
        [self sendTextMessage:string];
    }
    else if ([string hasPrefix:kMXKSlashCmdChangeDisplayName])
    {
        // Change display name
        NSString *displayName;
        
        // Sanity check
        if (string.length > kMXKSlashCmdChangeDisplayName.length)
        {
            displayName = [string substringFromIndex:kMXKSlashCmdChangeDisplayName.length + 1];
            
            // Remove white space from both ends
            displayName = [displayName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        }
        
        if (displayName.length)
        {
            [roomDataSource.mxSession.matrixRestClient setDisplayName:displayName success:^{
                
            } failure:^(NSError *error) {
                
                MXLogDebug(@"[MXKRoomVC] Set displayName failed");
                // Notify MatrixKit user
                NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
                
            }];
        }
        else
        {
            // Display cmd usage in text input as placeholder
            cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeDisplayName];
        }
    }
    else if ([string hasPrefix:kMXKSlashCmdJoinRoom])
    {
        // Join a room
        NSString *roomAlias;
        
        // Sanity check
        if (string.length > kMXKSlashCmdJoinRoom.length)
        {
            roomAlias = [string substringFromIndex:kMXKSlashCmdJoinRoom.length + 1];
            
            // Remove white space from both ends
            roomAlias = [roomAlias stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        }
        
        // Check
        if (roomAlias.length)
        {
            // TODO: /join command does not support via parameters yet
            [roomDataSource.mxSession joinRoom:roomAlias viaServers:nil success:^(MXRoom *room) {
                // Do nothing by default when we succeed to join the room
            } failure:^(NSError *error) {
                
                MXLogDebug(@"[MXKRoomVC] Join roomAlias (%@) failed", roomAlias);
                // Notify MatrixKit user
                NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
                
            }];
        }
        else
        {
            // Display cmd usage in text input as placeholder
            cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom];
        }
    }
    else if ([string hasPrefix:kMXKSlashCmdPartRoom])
    {
        // Leave this room or another one
        NSString *roomId;
        NSString *roomIdOrAlias;
        
        // Sanity check
        if (string.length > kMXKSlashCmdPartRoom.length)
        {
            roomIdOrAlias = [string substringFromIndex:kMXKSlashCmdPartRoom.length + 1];
            
            // Remove white space from both ends
            roomIdOrAlias = [roomIdOrAlias stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        }

        // Check
        if (roomIdOrAlias.length)
        {
            // Leave another room
            if ([MXTools isMatrixRoomAlias:roomIdOrAlias])
            {
                // Convert the alias to a room ID
                MXRoom *room = [roomDataSource.mxSession roomWithAlias:roomIdOrAlias];
                if (room)
                {
                    roomId = room.roomId;
                }
            }
            else if ([MXTools isMatrixRoomIdentifier:roomIdOrAlias])
            {
                roomId = roomIdOrAlias;
            }
        }
        else
        {
            // Leave the current room
            roomId = roomDataSource.roomId;
        }

        if (roomId.length)
        {
            [roomDataSource.mxSession leaveRoom:roomId success:^{

            } failure:^(NSError *error) {

                MXLogDebug(@"[MXKRoomVC] Part room_alias (%@ / %@) failed", roomIdOrAlias, roomId);
                // Notify MatrixKit user
                NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
                
            }];
        }
        else
        {
            // Display cmd usage in text input as placeholder
            cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandPartRoom];
        }
    }
    else if ([string hasPrefix:kMXKSlashCmdChangeRoomTopic])
    {
        // Change topic
        NSString *topic;
        
        // Sanity check
        if (string.length > kMXKSlashCmdChangeRoomTopic.length)
        {
            topic = [string substringFromIndex:kMXKSlashCmdChangeRoomTopic.length + 1];
            // Remove white space from both ends
            topic = [topic stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
        }

        if (topic.length)
        {
            [roomDataSource.room setTopic:topic success:^{
                
            } failure:^(NSError *error) {

                MXLogDebug(@"[MXKRoomVC] Set topic failed");
                // Notify MatrixKit user
                NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];

            }];
        }
        else
        {
            // Display cmd usage in text input as placeholder
            cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeRoomTopic];
        }
    }
    else if ([string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandDiscardSession]])
    {
        [roomDataSource.mxSession.crypto discardOutboundGroupSessionForRoomWithRoomId:roomDataSource.roomId onComplete:^{
            MXLogDebug(@"[MXKRoomVC] Manually discarded outbound group session");
        }];
    }
    else
    {
        // Retrieve userId
        NSString *userId = nil;
        while (index < components.count)
        {
            userId = [components objectAtIndex:index++];
            if (userId.length)
            {
                // done
                break;
            }
            // reset
            userId = nil;
        }
        
        if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandInviteUser]])
        {
            if (userId)
            {
                // Invite the user
                [roomDataSource.room inviteUser:userId success:^{

                } failure:^(NSError *error) {

                    MXLogDebug(@"[MXKRoomVC] Invite user (%@) failed", userId);
                    // Notify MatrixKit user
                    NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];

                }];
            }
            else
            {
                // Display cmd usage in text input as placeholder
                cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandInviteUser];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandKickUser]])
        {
            if (userId)
            {
                // Retrieve potential reason
                NSString *reason = nil;
                while (index < components.count)
                {
                    if (reason)
                    {
                        reason = [NSString stringWithFormat:@"%@ %@", reason, [components objectAtIndex:index++]];
                    }
                    else
                    {
                        reason = [components objectAtIndex:index++];
                    }
                }
                // Kick the user
                [roomDataSource.room kickUser:userId reason:reason success:^{
                    
                } failure:^(NSError *error) {
                    
                    MXLogDebug(@"[MXKRoomVC] Kick user (%@) failed", userId);
                    // Notify MatrixKit user
                    NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
                    
                }];
            }
            else
            {
                // Display cmd usage in text input as placeholder
                cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandKickUser];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandBanUser]])
        {
            if (userId)
            {
                // Retrieve potential reason
                NSString *reason = nil;
                while (index < components.count)
                {
                    if (reason)
                    {
                        reason = [NSString stringWithFormat:@"%@ %@", reason, [components objectAtIndex:index++]];
                    }
                    else
                    {
                        reason = [components objectAtIndex:index++];
                    }
                }
                // Ban the user
                [roomDataSource.room banUser:userId reason:reason success:^{
                    
                } failure:^(NSError *error) {
                    
                    MXLogDebug(@"[MXKRoomVC] Ban user (%@) failed", userId);
                    // Notify MatrixKit user
                    NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
                    
                }];
            }
            else
            {
                // Display cmd usage in text input as placeholder
                cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandBanUser];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandUnbanUser]])
        {
            if (userId)
            {
                // Unban the user
                [roomDataSource.room unbanUser:userId success:^{
                    
                } failure:^(NSError *error) {
                    
                    MXLogDebug(@"[MXKRoomVC] Unban user (%@) failed", userId);
                    // Notify MatrixKit user
                    NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
                    
                }];
            }
            else
            {
                // Display cmd usage in text input as placeholder
                cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandUnbanUser];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandSetUserPowerLevel]])
        {
            // Retrieve power level
            NSString *powerLevel = nil;
            while (index < components.count)
            {
                powerLevel = [components objectAtIndex:index++];
                if (powerLevel.length)
                {
                    // done
                    break;
                }
                // reset
                powerLevel = nil;
            }
            // Set power level
            if (userId && powerLevel)
            {
                // Set user power level
                [roomDataSource.room setPowerLevelOfUserWithUserID:userId powerLevel:[powerLevel integerValue] success:^{
                    
                } failure:^(NSError *error) {
                    
                    MXLogDebug(@"[MXKRoomVC] Set user power (%@) failed", userId);
                    // Notify MatrixKit user
                    NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
                    
                }];
            }
            else
            {
                // Display cmd usage in text input as placeholder
                cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandSetUserPowerLevel];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandResetUserPowerLevel]])
        {
            if (userId)
            {
                // Reset user power level
                [roomDataSource.room setPowerLevelOfUserWithUserID:userId powerLevel:0 success:^{

                } failure:^(NSError *error) {

                    MXLogDebug(@"[MXKRoomVC] Reset user power (%@) failed", userId);
                    // Notify MatrixKit user
                    NSString *myUserId = self->roomDataSource.mxSession.myUser.userId;
                    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];

                }];
            }
            else
            {
                // Display cmd usage in text input as placeholder
                cmdUsage = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandResetUserPowerLevel];
            }
        }
        else
        {
            MXLogDebug(@"[MXKRoomVC] Unrecognised IRC-style command: %@", string);
//            cmdUsage = [NSString stringWithFormat:@"Unrecognised IRC-style command: %@", cmd];
            return NO;
        }
    }
    return YES;
}

- (void)mention:(MXRoomMember*)roomMember
{
    NSString *memberName = roomMember.displayname.length ? roomMember.displayname : roomMember.userId;

    if (inputToolbarView.textMessage.length)
    {
        [inputToolbarView pasteText:memberName];
    }
    else if ([roomMember.userId isEqualToString:self.mainSession.myUser.userId])
    {
        // Prepare emote
        inputToolbarView.textMessage = @"/me ";
    }
    else
    {
        // Bing the member
        inputToolbarView.textMessage = [NSString stringWithFormat:@"%@: ", memberName];
    }
    
    [inputToolbarView becomeFirstResponder];
}

- (void)dismissKeyboard
{
    [titleView dismissKeyboard];
    [inputToolbarView dismissKeyboard];
}

- (BOOL)isBubblesTableScrollViewAtTheBottom
{
    if (_bubblesTableView.contentSize.height)
    {
        // Check whether the most recent message is visible.
        // Compute the max vertical position visible according to contentOffset
        CGFloat maxPositionY = _bubblesTableView.contentOffset.y + (_bubblesTableView.frame.size.height - _bubblesTableView.adjustedContentInset.bottom);
        // Be a bit less retrictive, consider the table view at the bottom even if the most recent message is partially hidden
        maxPositionY += 44;
        BOOL isScrolledToBottom = (maxPositionY >= _bubblesTableView.contentSize.height);

        // Consider the table view at the bottom if a scrolling to bottom is in progress too
        return (isScrolledToBottom || isScrollingToBottom);
    }
    
    // Consider empty table view as at the bottom. Only do this after it has appeared.
    // Returning YES here before the view has appeared allows calls to scrollBubblesTableViewToBottomAnimated
    // before the view knows its final size, resulting in a position offset the second time a room is shown (#4524).
    return hasAppearedOnce;
}

- (void)scrollBubblesTableViewToBottomAnimated:(BOOL)animated
{
    if (_bubblesTableView.contentSize.height)
    {
        CGFloat visibleHeight = _bubblesTableView.frame.size.height - _bubblesTableView.adjustedContentInset.top - _bubblesTableView.adjustedContentInset.bottom;
        if (visibleHeight < _bubblesTableView.contentSize.height)
        {
            CGFloat wantedOffsetY = _bubblesTableView.contentSize.height - visibleHeight - _bubblesTableView.adjustedContentInset.top;
            CGFloat currentOffsetY = _bubblesTableView.contentOffset.y;
            if (wantedOffsetY != currentOffsetY)
            {
                isScrollingToBottom = YES;
                BOOL savedBubbleTableViewDisplayInTransition = self.isBubbleTableViewDisplayInTransition;
                self.bubbleTableViewDisplayInTransition = YES;
                [self setBubbleTableViewContentOffset:CGPointMake(0, wantedOffsetY) animated:animated];
                self.bubbleTableViewDisplayInTransition = savedBubbleTableViewDisplayInTransition;
            }
            else
            {
                // upateCurrentEventIdAtTableBottom must be called here (it is usually called by the scrollview delegate at the end of scrolling).
                [self updateCurrentEventIdAtTableBottom:YES];
            }
        }
        else
        {
            [self setBubbleTableViewContentOffset:CGPointMake(0, -_bubblesTableView.adjustedContentInset.top) animated:animated];            
        }
        
        shouldScrollToBottomOnTableRefresh = NO;
    }
}

- (void)dismissTemporarySubViews
{
    [self dismissKeyboard];
    
    if (currentAlert)
    {
        [currentAlert dismissViewControllerAnimated:NO completion:nil];
        currentAlert = nil;
    }
    
    if (eventDetailsView)
    {
        [eventDetailsView removeFromSuperview];
        eventDetailsView = nil;
    }
    
    if (_leftRoomReasonLabel)
    {
        [_leftRoomReasonLabel removeFromSuperview];
        _leftRoomReasonLabel = nil;
        _bubblesTableView.tableHeaderView = nil;
    }
    
    // Dispose potential keyboard view
    self.keyboardView = nil;
}

- (void)setBubbleTableViewContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
    if (preventBubblesTableViewScroll)
    {
        return;
    }
    
    [self.bubblesTableView setContentOffset:contentOffset animated:animated];
}

#pragma mark - properties

- (void)setBubbleTableViewDisplayInTransition:(BOOL)bubbleTableViewDisplayInTransition
{
    if (_bubbleTableViewDisplayInTransition != bubbleTableViewDisplayInTransition)
    {
        _bubbleTableViewDisplayInTransition = bubbleTableViewDisplayInTransition;
        
        [self updateCurrentEventIdAtTableBottom:YES];
    }
}

- (void)setUpdateRoomReadMarker:(BOOL)updateRoomReadMarker
{
    if (_updateRoomReadMarker != updateRoomReadMarker)
    {
        _updateRoomReadMarker = updateRoomReadMarker;
        
        if (updateRoomReadMarker == YES)
        {
            if (currentEventIdAtTableBottom)
            {
                [self.roomDataSource.room moveReadMarkerToEventId:currentEventIdAtTableBottom];
            }
            else
            {
                // Look for the last displayed event.
                [self updateCurrentEventIdAtTableBottom:YES];
            }
        }
    }
}

#pragma mark - activity indicator

- (BOOL)canStopActivityIndicator {
    // Keep the loading wheel displayed while we are joining the room
    if (joinRoomRequest)
    {
        return NO;
    }
    
    // Check internal processes before stopping the loading wheel
    if (isPaginationInProgress || isInputToolbarProcessing)
    {
        // Keep activity indicator running
        return NO;
    }
    
    return [super canStopActivityIndicator];
}

#pragma mark - Pagination

- (void)triggerInitialBackPagination
{
    // Trigger back pagination to fill all the screen
    CGRect frame = [[UIScreen mainScreen] bounds];
    
    MXWeakify(self);
    
    isPaginationInProgress = YES;
    [self startActivityIndicator];
    [roomDataSource paginateToFillRect:frame
                             direction:MXTimelineDirectionBackwards
           withMinRequestMessagesCount:_paginationLimit
                               success:^{
        
                                   MXStrongifyAndReturnIfNil(self);

                                   // Stop spinner
                                   self->isPaginationInProgress = NO;
                                   [self stopActivityIndicator];
                                   
                                   self.bubbleTableViewDisplayInTransition = YES;

                                   // Reload table
                                   [self reloadBubblesTable:YES];

                                   if (self->roomDataSource.timeline.initialEventId)
                                   {
                                       // Center the table view to the cell that contains this event
                                       NSInteger index = [self->roomDataSource indexOfCellDataWithEventId:self->roomDataSource.timeline.initialEventId];
                                       if (index != NSNotFound)
                                       {
                                           // Let iOS put the cell at the top of the table view
                                           [self.bubblesTableView scrollToRowAtIndexPath: [NSIndexPath indexPathForRow:index inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
                                           
                                           // Apply an offset to move the targeted component at the center of the screen.
                                           UITableViewCell *cell = [self->_bubblesTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
                                           
                                           CGPoint contentOffset = self->_bubblesTableView.contentOffset;
                                           CGFloat firstVisibleContentRowOffset = self->_bubblesTableView.contentOffset.y + self->_bubblesTableView.adjustedContentInset.top;
                                           CGFloat lastVisibleContentRowOffset = self->_bubblesTableView.frame.size.height - self->_bubblesTableView.adjustedContentInset.bottom;
                                           
                                           CGFloat localPositionOfEvent = 0.0;
                                           
                                           if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
                                           {
                                               MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
                                               
                                               if (self->_centerBubblesTableViewContentOnTheInitialEventBottom)
                                               {
                                                   localPositionOfEvent = [roomBubbleTableViewCell bottomPositionOfEvent:self->roomDataSource.timeline.initialEventId];
                                               }
                                               else
                                               {
                                                   localPositionOfEvent = [roomBubbleTableViewCell topPositionOfEvent:self->roomDataSource.timeline.initialEventId];
                                               }
                                           }
                                           
                                           contentOffset.y += localPositionOfEvent - (lastVisibleContentRowOffset / 2 - (cell.frame.origin.y - firstVisibleContentRowOffset));
                                           
                                           // Sanity check
                                           if (contentOffset.y + lastVisibleContentRowOffset > self->_bubblesTableView.contentSize.height)
                                           {
                                               contentOffset.y = self->_bubblesTableView.contentSize.height - lastVisibleContentRowOffset;
                                           }
                                           
                                           [self setBubbleTableViewContentOffset:contentOffset animated:NO];
                                           
                                           
                                           // Update the read receipt and potentially the read marker.
                                           [self updateCurrentEventIdAtTableBottom:YES];
                                       }
                                   }
                                   
                                   self.bubbleTableViewDisplayInTransition = NO;
                               }
                               failure:^(NSError *error) {
        
                                   MXStrongifyAndReturnIfNil(self);

                                   // Stop spinner
                                   self->isPaginationInProgress = NO;
                                   [self stopActivityIndicator];
                                   
                                   self.bubbleTableViewDisplayInTransition = YES;

                                   // Reload table
                                   [self reloadBubblesTable:YES];
                                   
                                   self.bubbleTableViewDisplayInTransition = NO;

                               }];
}

/**
 Trigger an inconspicuous pagination.
 The retrieved history is added discretely to the top or the bottom of bubbles table without change the current display.

 @param limit the maximum number of messages to retrieve.
 @param direction backwards or forwards.
 */
- (void)triggerPagination:(NSUInteger)limit direction:(MXTimelineDirection)direction
{
    // Paginate only if possible
    if (isPaginationInProgress || roomDataSource.state != MXKDataSourceStateReady || NO == [roomDataSource.timeline canPaginate:direction])
    {
        return;
    }
    
    UserIndicatorCancel cancelIndicator = [self.userIndicatorStore presentLoadingWithLabel:[VectorL10n loading] isInteractionBlocking:NO];
    
    // Store the current height of the first bubble (if any)
    backPaginationSavedFirstBubbleHeight = 0;
    if (direction == MXTimelineDirectionBackwards && [roomDataSource tableView:_bubblesTableView numberOfRowsInSection:0])
    {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
        backPaginationSavedFirstBubbleHeight = [self tableView:_bubblesTableView heightForRowAtIndexPath:indexPath];
    }
    
    isPaginationInProgress = YES;
    
    MXWeakify(self);
    
    // Trigger pagination
    [roomDataSource paginate:limit direction:direction onlyFromStore:NO success:^(NSUInteger addedCellNumber) {
        
        MXStrongifyAndReturnIfNil(self);
        
        // We will adjust the vertical offset in order to unchange the current display (pagination should be inconspicuous)
        CGFloat verticalOffset = 0;

        if (direction == MXTimelineDirectionBackwards)
        {
            // Compute the cumulative height of the added messages
            for (NSUInteger index = 0; index < addedCellNumber; index++)
            {
                NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
                verticalOffset += [self tableView:self->_bubblesTableView heightForRowAtIndexPath:indexPath];
            }

            // Add delta of the height of the previous first cell (if any)
            if (addedCellNumber < [self->roomDataSource tableView:self->_bubblesTableView numberOfRowsInSection:0])
            {
                NSIndexPath *indexPath = [NSIndexPath indexPathForRow:addedCellNumber inSection:0];
                verticalOffset += ([self tableView:self->_bubblesTableView heightForRowAtIndexPath:indexPath] - self->backPaginationSavedFirstBubbleHeight);
            }

            self->_bubblesTableView.tableHeaderView = self->backPaginationActivityView = nil;
        }
        else
        {
            self->_bubblesTableView.tableFooterView = self->reconnectingView = nil;
        }

        // Trigger a full table reload. We could not only insert new cells related to pagination,
        // because some other changes may have been ignored during pagination (see[dataSource:didCellChange:]).
        self.bubbleTableViewDisplayInTransition = YES;

        // Disable temporarily scrolling and hide the scroll indicator during refresh to prevent flickering
        [self.bubblesTableView setShowsVerticalScrollIndicator:NO];
        [self.bubblesTableView setScrollEnabled:NO];

        CGPoint contentOffset = self.bubblesTableView.contentOffset;

        BOOL hasBeenScrolledToBottom = [self reloadBubblesTable:NO];

        if (direction == MXTimelineDirectionBackwards)
        {
            // Backwards pagination adds cells at the top of the tableview content.
            // Vertical content offset needs to be updated (except if the table has been scrolled to bottom)
            if ((!hasBeenScrolledToBottom && verticalOffset > 0) || direction == MXTimelineDirectionForwards)
            {
                // Adjust vertical offset in order to compensate scrolling
                contentOffset.y += verticalOffset;
                [self setBubbleTableViewContentOffset:contentOffset animated:NO];
            }
        }
        else
        {
            [self setBubbleTableViewContentOffset:contentOffset animated:NO];
        }

        // Restore scrolling and the scroll indicator
        [self.bubblesTableView setShowsVerticalScrollIndicator:YES];
        [self.bubblesTableView setScrollEnabled:YES];

        self.bubbleTableViewDisplayInTransition = NO;
        self->isPaginationInProgress = NO;

        // Force the update of the current visual position
        // Else there is a scroll jump on incoming message (see https://github.com/vector-im/vector-ios/issues/79)
        if (direction == MXTimelineDirectionBackwards)
        {
            [self updateCurrentEventIdAtTableBottom:NO];
        }
        
        if (cancelIndicator) {
            cancelIndicator();
        }

    } failure:^(NSError *error) {
        
        MXStrongifyAndReturnIfNil(self);
        
        self.bubbleTableViewDisplayInTransition = YES;
        
        // Reload table on failure because some changes may have been ignored during pagination (see[dataSource:didCellChange:])
        self->isPaginationInProgress = NO;
        self->_bubblesTableView.tableHeaderView = self->backPaginationActivityView = nil;
        
        [self reloadBubblesTable:NO];
        
        self.bubbleTableViewDisplayInTransition = NO;

        if (cancelIndicator) {
            cancelIndicator();
        }
    }];
}

- (void)triggerAttachmentBackPagination:(NSString*)eventId
{
    // Paginate only if possible
    if (NO == [roomDataSource.timeline canPaginate:MXTimelineDirectionBackwards] && self.attachmentsViewer)
    {
        return;
    }
    
    isPaginationInProgress = YES;
    
    MXWeakify(self);
    
    // Trigger back pagination to find previous attachments
    [roomDataSource paginate:_paginationLimit direction:MXTimelineDirectionBackwards onlyFromStore:NO success:^(NSUInteger addedCellNumber) {
        
        MXStrongifyAndReturnIfNil(self);
        
        // Check whether attachments viewer is still visible
        if (self.attachmentsViewer)
        {
            // Check whether some older attachments have been added.
            // Note: the stickers are excluded from the attachments list returned by the room datasource.
            BOOL isDone = NO;
            NSArray *attachmentsWithThumbnail = self.roomDataSource.attachmentsWithThumbnail;
            if (attachmentsWithThumbnail.count)
            {
                MXKAttachment *attachment = attachmentsWithThumbnail.firstObject;
                isDone = ![attachment.eventId isEqualToString:eventId];
            }
            
            // Check whether pagination is still available
            self.attachmentsViewer.complete = ([self->roomDataSource.timeline canPaginate:MXTimelineDirectionBackwards] == NO);
            
            if (isDone || self.attachmentsViewer.complete)
            {
                // Refresh the current attachments list.
                [self.attachmentsViewer displayAttachments:attachmentsWithThumbnail focusOn:nil];
                
                // Trigger a full table reload without scrolling. We could not only insert new cells related to back pagination,
                // because some other changes may have been ignored during back pagination (see[dataSource:didCellChange:]).
                self.bubbleTableViewDisplayInTransition = YES;
                self->isPaginationInProgress = NO;
                [self reloadBubblesTable:YES];
                self.bubbleTableViewDisplayInTransition = NO;
                
                // Done
                return;
            }
            
            // Here a new back pagination is required
            [self triggerAttachmentBackPagination:eventId];
        }
        else
        {
            // Trigger a full table reload without scrolling. We could not only insert new cells related to back pagination,
            // because some other changes may have been ignored during back pagination (see[dataSource:didCellChange:]).
            self.bubbleTableViewDisplayInTransition = YES;
            self->isPaginationInProgress = NO;
            [self reloadBubblesTable:YES];
            self.bubbleTableViewDisplayInTransition = NO;
        }
        
    } failure:^(NSError *error) {
        
        MXStrongifyAndReturnIfNil(self);
        
        // Reload table on failure because some changes may have been ignored during back pagination (see[dataSource:didCellChange:])
        self.bubbleTableViewDisplayInTransition = YES;
        self->isPaginationInProgress = NO;
        [self reloadBubblesTable:YES];
        self.bubbleTableViewDisplayInTransition = NO;
        
        if (self.attachmentsViewer)
        {
            // Force attachments update to cancel potential loading wheel
            [self.attachmentsViewer displayAttachments:self.attachmentsViewer.attachments focusOn:nil];
        }
        
    }];
}

#pragma mark - Post messages

- (void)sendTextMessage:(NSString*)msgTxt
{
    // Let the datasource send it and manage the local echo
    [roomDataSource sendTextMessage:msgTxt success:nil failure:^(NSError *error)
    {
        // Just log the error. The message will be displayed in red in the room history
        MXLogDebug(@"[MXKRoomViewController] sendTextMessage failed.");
    }];
}

# pragma mark - Event handling

- (void)showEventDetails:(MXEvent *)event
{
    [self dismissKeyboard];
    
    // Remove potential existing subviews
    [self dismissTemporarySubViews];
    
    MXKEventDetailsView *eventDetailsView;
    
    if (customEventDetailsViewClass)
    {
        eventDetailsView = [[customEventDetailsViewClass alloc] initWithEvent:event andMatrixSession:roomDataSource.mxSession];
    }
    else
    {
        eventDetailsView = [[MXKEventDetailsView alloc] initWithEvent:event andMatrixSession:roomDataSource.mxSession];
    }    
    
    // Add shadow on event details view
    eventDetailsView.layer.cornerRadius = 5;
    eventDetailsView.layer.shadowOffset = CGSizeMake(0, 1);
    eventDetailsView.layer.shadowOpacity = 0.5f;
    
    // Add the view and define edge constraints
    [self.view addSubview:eventDetailsView];
    
    self->eventDetailsView = eventDetailsView;
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated"
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:eventDetailsView
                                                          attribute:NSLayoutAttributeTop
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.topLayoutGuide
                                                          attribute:NSLayoutAttributeBottom
                                                         multiplier:1.0f
                                                           constant:10.0f]];
    
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:eventDetailsView
                                                          attribute:NSLayoutAttributeBottom
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.bottomLayoutGuide
                                                          attribute:NSLayoutAttributeTop
                                                         multiplier:1.0f
                                                           constant:-10.0f]];
    #pragma clang diagnostic pop
    
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view
                                                          attribute:NSLayoutAttributeLeading
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:eventDetailsView
                                                          attribute:NSLayoutAttributeLeading
                                                         multiplier:1.0f
                                                           constant:-10.0f]];
    
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view
                                                          attribute:NSLayoutAttributeTrailing
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:eventDetailsView
                                                          attribute:NSLayoutAttributeTrailing
                                                         multiplier:1.0f
                                                           constant:10.0f]];
    [self.view setNeedsUpdateConstraints];
}

- (void)promptUserToResendEvent:(NSString *)eventId
{
    MXEvent *event = [roomDataSource eventWithEventId:eventId];
    
    MXLogDebug(@"[MXKRoomViewController] promptUserToResendEvent: %@", event);
    
    if (event && event.eventType == MXEventTypeRoomMessage)
    {
        NSString *msgtype = event.content[kMXMessageTypeKey];
        
        NSString* textMessage;
        if ([msgtype isEqualToString:kMXMessageTypeText])
        {
            textMessage = event.content[kMXMessageBodyKey];
        }
        
        // Show a confirmation popup to the end user
        if (currentAlert)
        {
            [currentAlert dismissViewControllerAnimated:NO completion:nil];
        }
        
        __weak typeof(self) weakSelf = self;
        
        UIAlertController *resendAlert = [UIAlertController alertControllerWithTitle:[VectorL10n resendMessage]
                                                                             message:textMessage
                                                                      preferredStyle:UIAlertControllerStyleAlert];
        
        [resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction * action) {
                                                           
                                                           typeof(self) self = weakSelf;
                                                           self->currentAlert = nil;
                                                           
                                                       }]];
        
        [resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction * action) {
                                                           
                                                           typeof(self) self = weakSelf;
                                                           self->currentAlert = nil;
                                                           
                                                           // Let the datasource resend. It will manage local echo, etc.
                                                           [self->roomDataSource resendEventWithEventId:eventId success:nil failure:nil];
                                                           
                                                       }]];
        
        [self presentViewController:resendAlert animated:YES completion:nil];
        currentAlert = resendAlert;
    }
}

#pragma mark - bubbles table

- (BOOL)reloadBubblesTable:(BOOL)useBottomAnchor
{
    return [self reloadBubblesTable:useBottomAnchor invalidateBubblesCellDataCache:NO];
}

- (BOOL)reloadBubblesTable:(BOOL)useBottomAnchor invalidateBubblesCellDataCache:(BOOL)invalidateBubblesCellDataCache
{
    BOOL shouldScrollToBottom = shouldScrollToBottomOnTableRefresh;
    
    // When no size transition is in progress, check if the bottom of the content is currently visible.
    // If this is the case, we will scroll automatically to the bottom after table refresh.
    if (!isSizeTransitionInProgress && !shouldScrollToBottom)
    {
        shouldScrollToBottom = [self isBubblesTableScrollViewAtTheBottom];
    }
    
    // Force bubblesCellData message recalculation if requested
    if (invalidateBubblesCellDataCache)
    {
        [self.roomDataSource invalidateBubblesCellDataCache];
    }
    
    // When scroll to bottom is not active, check whether we should keep the current event displayed at the bottom of the table
    if (!shouldScrollToBottom && useBottomAnchor && currentEventIdAtTableBottom)
    {
        // Update content offset after refresh in order to keep visible the current event displayed at the bottom
        
        [_bubblesTableView reloadData];
        
        // Retrieve the new cell index of the event displayed previously at the bottom of table
        NSInteger rowIndex = [roomDataSource indexOfCellDataWithEventId:currentEventIdAtTableBottom];
        if (rowIndex != NSNotFound)
        {
            // Retrieve the corresponding cell
            UITableViewCell *cell = [_bubblesTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:rowIndex inSection:0]];
            UITableViewCell *cellTmp;
            if (!cell)
            {
                NSString *reuseIdentifier = [self cellReuseIdentifierForCellData:[roomDataSource cellDataAtIndex:rowIndex]];
                // Create temporarily the cell (this cell will released at the end, to be reusable)
                // Do not pass in the indexPath when creating this cell, as there is a possible crash by dequeuing
                // multiple cells for the same index path if rotating the device coincides with reloading the data.
                cellTmp = [_bubblesTableView dequeueReusableCellWithIdentifier:reuseIdentifier];
                cell = cellTmp;
            }
            
            if (cell)
            {
                CGFloat eventTopPosition = cell.frame.origin.y;
                CGFloat eventBottomPosition = eventTopPosition + cell.frame.size.height;
                
                // Compute accurate event positions in case of bubble with multiple components
                if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
                {
                    MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
                    NSArray *bubbleComponents = roomBubbleTableViewCell.bubbleData.bubbleComponents;
                    
                    if (bubbleComponents.count > 1)
                    {
                        // Check and update each component position
                        [roomBubbleTableViewCell.bubbleData prepareBubbleComponentsPosition];
                        
                        NSInteger index = bubbleComponents.count - 1;
                        MXKRoomBubbleComponent *component = bubbleComponents[index];
                        
                        if ([component.event.eventId isEqualToString:currentEventIdAtTableBottom])
                        {
                            eventTopPosition += roomBubbleTableViewCell.msgTextViewTopConstraint.constant + component.position.y;
                        }
                        else
                        {
                            while (index--)
                            {
                                MXKRoomBubbleComponent *previousComponent = bubbleComponents[index];
                                if ([previousComponent.event.eventId isEqualToString:currentEventIdAtTableBottom])
                                {
                                    // Update top position if this is not the first component
                                    if (index)
                                    {
                                        eventTopPosition += roomBubbleTableViewCell.msgTextViewTopConstraint.constant + previousComponent.position.y;
                                    }
                                    
                                    eventBottomPosition = cell.frame.origin.y + roomBubbleTableViewCell.msgTextViewTopConstraint.constant + component.position.y;
                                    break;
                                }
                                
                                component = previousComponent;
                            }
                        }
                    }
                }
                
                // Compute the offset of the content displayed at the bottom.
                CGFloat contentBottomOffsetY = _bubblesTableView.contentOffset.y + (_bubblesTableView.frame.size.height - _bubblesTableView.adjustedContentInset.bottom);
                if (contentBottomOffsetY > _bubblesTableView.contentSize.height)
                {
                    contentBottomOffsetY = _bubblesTableView.contentSize.height;
                }
                
                // Check whether this event is no more displayed at the bottom
                if ((contentBottomOffsetY <= eventTopPosition ) || (eventBottomPosition < contentBottomOffsetY))
                {
                    // Compute the top content offset to display again this event at the table bottom
                    CGFloat contentOffsetY = eventBottomPosition - (_bubblesTableView.frame.size.height - _bubblesTableView.adjustedContentInset.bottom);
                    
                    // Check if there are enought data to fill the top
                    if (contentOffsetY < -_bubblesTableView.adjustedContentInset.top)
                    {
                        // Scroll to the top
                        contentOffsetY = -_bubblesTableView.adjustedContentInset.top;
                    }
                    
                    CGPoint contentOffset = _bubblesTableView.contentOffset;
                    contentOffset.y = contentOffsetY;
                    [self setBubbleTableViewContentOffset:contentOffset animated:NO];
                }
                
                if (cellTmp && [cellTmp conformsToProtocol:@protocol(MXKCellRendering)] && [cellTmp respondsToSelector:@selector(didEndDisplay)])
                {
                    // Release here resources, and restore reusable cells
                    [(id<MXKCellRendering>)cellTmp didEndDisplay];
                }
            }
        }
    }
    else
    {
        // Do a full reload
        [_bubblesTableView reloadData];
        if (shouldScrollToBottom) {
            // If we need to scroll to the bottom after the reload, layout refresh needs to be triggered,
            // otherwise contentSize of the table view will not be up-to-date
            // e.g. https://stackoverflow.com/a/31324129
            [_bubblesTableView layoutIfNeeded];
        }
    }
    
    if (shouldScrollToBottom)
    {
        [self scrollBubblesTableViewToBottomAnimated:NO];
    }
    
    return shouldScrollToBottom;
}

- (void)updateCurrentEventIdAtTableBottom:(BOOL)acknowledge
{
    // Do not update events if the controller is used as context menu preview.
    if (self.isContextPreview)
    {
        return;
    }
    
    // Update the identifier of the event displayed at the bottom of the table, except if a rotation or other size transition is in progress.
    if (!isSizeTransitionInProgress && !self.isBubbleTableViewDisplayInTransition)
    {
        // Compute the content offset corresponding to the line displayed at the table bottom (just above the toolbar).
        CGFloat contentBottomOffsetY = _bubblesTableView.contentOffset.y + (_bubblesTableView.frame.size.height - _bubblesTableView.adjustedContentInset.bottom);
        if (contentBottomOffsetY > _bubblesTableView.contentSize.height)
        {
            contentBottomOffsetY = _bubblesTableView.contentSize.height;
        }
        // Be a bit less retrictive, consider visible an event at the bottom even if is partially hidden.
        contentBottomOffsetY += kCellVisibilityMinimumHeight;
        
        // Reset the current event id
        currentEventIdAtTableBottom = nil;
        
        // Consider the visible cells (starting by those displayed at the bottom)
        NSArray *visibleCells = [_bubblesTableView visibleCells];
        NSInteger index = visibleCells.count;
        UITableViewCell *cell;
        while (index--)
        {
            cell = visibleCells[index];
            
            // Check whether the cell is actually visible
            if (cell && (cell.frame.origin.y < contentBottomOffsetY))
            {
                if (![cell isKindOfClass:MXKTableViewCell.class])
                {
                    continue;
                }
                
                MXKCellData *cellData = ((MXKTableViewCell *)cell).mxkCellData;
                
                // Only 'MXKRoomBubbleCellData' is supported here for the moment.
                if (![cellData isKindOfClass:MXKRoomBubbleCellData.class])
                {
                    continue;
                }

                MXKRoomBubbleCellData *bubbleData = (MXKRoomBubbleCellData*)cellData;
                
                // Check which bubble component is displayed at the bottom.
                // For that update each component position.
                [bubbleData prepareBubbleComponentsPosition];
                
                NSArray *bubbleComponents = bubbleData.bubbleComponents;
                NSInteger componentIndex = bubbleComponents.count;
                
                CGFloat bottomPositionY = cell.frame.size.height;
                
                MXKRoomBubbleComponent *component;
                
                while (componentIndex --)
                {
                    component = bubbleComponents[componentIndex];
                    if (![cell isKindOfClass:MXKRoomBubbleTableViewCell.class])
                    {
                        continue;
                    }

                    MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
                    
                    // Check whether the bottom part of the component is visible.
                    CGFloat pos = cell.frame.origin.y + bottomPositionY;
                    if (pos <= contentBottomOffsetY)
                    {
                        // We found the component
                        currentEventIdAtTableBottom = component.event.eventId;
                        break;
                    }
                    
                    // Prepare the bottom position for the next component
                    bottomPositionY = roomBubbleTableViewCell.msgTextViewTopConstraint.constant + component.position.y;
                }
                
                if (currentEventIdAtTableBottom)
                {
                    if (acknowledge && self.isEventsAcknowledgementEnabled)
                    {
                        // Indicate to the homeserver that the user has read this event.
                        if (self.navigationController.viewControllers.lastObject == self)
                        {
                            // Check if the selected event is eligible to be the new read marker position too
                            if (!bubbleData.collapsed && [self eligibleForReadMarkerUpdate:component.event])
                            {
                                BOOL updateRoomReadMarker = _updateRoomReadMarker && [self isEventPosteriorToCurrentReadMarker:component.event];
                                // Acknowledge this event and update the read marker if needed
                                [roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:updateRoomReadMarker];
                            }
                            else
                            {
                                // Acknowledge only this event. The read marker is handled separately
                                [roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:NO];

                                if (_updateRoomReadMarker)
                                {
                                    // Try to find the best event for the new read marker position
                                    [self updateReadMarkerEvent];
                                }
                            }
                        }
                    }
                    break;
                }
                // else we consider the previous cell.
            }
        }
    }
}

- (BOOL)eligibleForReadMarkerUpdate:(MXEvent *)event {
    // Prevent the readmarker to be placed on a relatesTo or a redaction event
    if (event.relatesTo || event.redacts)
    {
        return NO;
    }
    
    return YES;
}

- (BOOL)isEventPosteriorToCurrentReadMarker:(MXEvent *)event {
    if (roomDataSource.room.accountData.readMarkerEventId)
    {
        MXEvent *currentReadMarkerEvent = [roomDataSource.mxSession.store eventWithEventId:roomDataSource.room.accountData.readMarkerEventId inRoom:roomDataSource.roomId];
        if (!currentReadMarkerEvent)
        {
            currentReadMarkerEvent = [roomDataSource eventWithEventId:roomDataSource.room.accountData.readMarkerEventId];
        }
        
        // Update the read marker only if the current event is available, and the new event is posterior to it.
        return currentReadMarkerEvent && (currentReadMarkerEvent.originServerTs <= event.originServerTs);
    }
    return YES;
}

/// Try to update the read marker by looking for an eligible event displayed at the bottom of the tableview
- (void)updateReadMarkerEvent
{
    // Compute the content offset corresponding to the line displayed at the table bottom (just above the toolbar).
    CGFloat contentBottomOffsetY = _bubblesTableView.contentOffset.y + (_bubblesTableView.frame.size.height - _bubblesTableView.adjustedContentInset.bottom);
    if (contentBottomOffsetY > _bubblesTableView.contentSize.height)
    {
        contentBottomOffsetY = _bubblesTableView.contentSize.height;
    }
    // Be a bit less retrictive, consider visible an event at the bottom even if is partially hidden.
    contentBottomOffsetY += kCellVisibilityMinimumHeight;
    
    // Consider the visible cells (starting by those displayed at the bottom)
    NSArray *visibleCells = [_bubblesTableView visibleCells];
    NSInteger index = visibleCells.count;
    UITableViewCell *cell;
    while (index--)
    {
        cell = visibleCells[index];
        
        // Check whether the cell is actually visible
        if (!cell || cell.frame.origin.y > contentBottomOffsetY)
        {
            continue;
        }

        if (![cell isKindOfClass:MXKRoomBubbleTableViewCell.class])
        {
            continue;
        }
        MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
        MXKRoomBubbleCellData *bubbleData = roomBubbleTableViewCell.bubbleData;
        if (!bubbleData)
        {
            continue;
        }
        
        // Prevent to place the read marker on a collapsed cell
        if (bubbleData.collapsed)
        {
            continue;
        }
        
        // Check which bubble component is displayed at the bottom.
        // For that update each component position.
        [bubbleData prepareBubbleComponentsPosition];
        
        NSArray *bubbleComponents = bubbleData.bubbleComponents;
        NSInteger componentIndex = bubbleComponents.count;
        
        CGFloat bottomPositionY = cell.frame.size.height;
        
        MXKRoomBubbleComponent *component;
        
        while (componentIndex --)
        {
            component = bubbleComponents[componentIndex];
            
            // Prevent the read marker to be placed on an unsupported event (e.g. redactions, reactions, ...)
            if (![self eligibleForReadMarkerUpdate:component.event])
            {
                continue;
            }
            
            // Check whether the bottom part of the component is visible.
            CGFloat pos = cell.frame.origin.y + bottomPositionY;
            if (pos <= contentBottomOffsetY)
            {
                // We found the component
                // Check whether the read marker must be updated.
                if ([self isEventPosteriorToCurrentReadMarker:component.event])
                {
                    // Move the read marker to this event
                    [roomDataSource.room moveReadMarkerToEventId:component.event.eventId];
                }
                
                return;
            }
            
            // Prepare the bottom position for the next component
            bottomPositionY = roomBubbleTableViewCell.msgTextViewTopConstraint.constant + component.position.y;
        }
        
        // else we consider the previous cell.
    }
}

#pragma mark - MXKDataSourceDelegate

- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
    return nil;
}

- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
    Class class = [self cellViewClassForCellData:cellData];
    
    if ([class respondsToSelector:@selector(defaultReuseIdentifier)])
    {
        return [class defaultReuseIdentifier];
    }
    
    return nil;
}

- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes
{
    UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
    if (sharedApplication && sharedApplication.applicationState != UIApplicationStateActive)
    {
        // Do nothing at the UI level if the application do a sync in background
        return;
    }

    if (isPaginationInProgress)
    {
        // Ignore these changes, the table will be full updated at the end of pagination.
        return;
    }
    
    if (self.attachmentsViewer)
    {
        // Refresh the current attachments list without changing the current displayed attachment (see focus = nil).
        NSArray *attachmentsWithThumbnail = self.roomDataSource.attachmentsWithThumbnail;
        [self.attachmentsViewer displayAttachments:attachmentsWithThumbnail focusOn:nil];
    }
    
    self.bubbleTableViewDisplayInTransition = YES;

    CGPoint contentOffset = self.bubblesTableView.contentOffset;

    BOOL hasScrolledToTheBottom = [self reloadBubblesTable:YES];

    // If the user is scrolling while we reload the data for a new incoming message for example,
    // there will be a jump in the table view display.
    // Resetting the contentOffset after the reload fixes the issue.
    if (hasScrolledToTheBottom == NO)
    {
        [self setBubbleTableViewContentOffset:contentOffset animated:NO];
    }
    
    self.bubbleTableViewDisplayInTransition = NO;
}

- (void)dataSource:(MXKDataSource *)dataSource didStateChange:(MXKDataSourceState)state
{
    [self updateViewControllerAppearanceOnRoomDataSourceState];
    
    if (state == MXKDataSourceStateReady)
    {
        [self onRoomDataSourceReady];
    }
}

- (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)actionIdentifier inCell:(id<MXKCellRendering>)cell userInfo:(NSDictionary *)userInfo
{
    MXLogDebug(@"Gesture %@ has been recognized in %@. UserInfo: %@", actionIdentifier, cell, userInfo);
    
    if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnMessageTextView])
    {
        MXLogDebug(@"    -> A message has been tapped");
    }
    else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnSenderNameLabel] || [actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAvatarView])
    {
//        MXLogDebug(@"    -> Name or avatar of %@ has been tapped", userInfo[kMXKRoomBubbleCellUserIdKey]);
        
        // Add the member display name in text input
        MXRoomMember *selectedRoomMember = [roomDataSource.roomState.members memberWithUserId:userInfo[kMXKRoomBubbleCellUserIdKey]];
        if (selectedRoomMember)
        {
            [self mention:selectedRoomMember];
        }
    }
    else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnDateTimeContainer])
    {
        roomDataSource.showBubblesDateTime = !roomDataSource.showBubblesDateTime;
        MXLogDebug(@"    -> Turn %@ cells date", roomDataSource.showBubblesDateTime ? @"ON" : @"OFF");
    }
    else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAttachmentView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
    {
        [self showAttachmentInCell:(MXKRoomBubbleTableViewCell *)cell];
    }
    else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnProgressView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
    {
        MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
        
        // Check if there is a download in progress, then offer to cancel it
        NSString *downloadId = roomBubbleTableViewCell.bubbleData.attachment.downloadId;
        if ([MXMediaManager existingDownloaderWithIdentifier:downloadId])
        {
            if (currentAlert)
            {
                [currentAlert dismissViewControllerAnimated:NO completion:nil];
            }
            
            __weak __typeof(self) weakSelf = self;
            UIAlertController *cancelAlert = [UIAlertController alertControllerWithTitle:nil
                                                                                 message:[VectorL10n attachmentCancelDownload]
                                                                          preferredStyle:UIAlertControllerStyleAlert];
            
            [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n no]
                                                            style:UIAlertActionStyleDefault
                                                          handler:^(UIAlertAction * action) {
                                                               
                                                               typeof(self) self = weakSelf;
                                                               self->currentAlert = nil;
                                                               
                                                           }]];
            
            [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n yes]
                                                            style:UIAlertActionStyleDefault
                                                          handler:^(UIAlertAction * action) {
                                                               
                                                               typeof(self) self = weakSelf;
                                                               self->currentAlert = nil;
                                                               
                                                               // Get again the loader
                                                               MXMediaLoader *loader = [MXMediaManager existingDownloaderWithIdentifier:downloadId];
                                                               if (loader)
                                                               {
                                                                   [loader cancel];
                                                               }
                                                               
                                                               // Hide the progress animation
                                                               roomBubbleTableViewCell.progressView.hidden = YES;
                                                               
                                                           }]];
            
            [self presentViewController:cancelAlert animated:YES completion:nil];
            currentAlert = cancelAlert;
        }
        else if (roomBubbleTableViewCell.bubbleData.attachment.eventSentState == MXEventSentStatePreparing ||
                 roomBubbleTableViewCell.bubbleData.attachment.eventSentState == MXEventSentStateEncrypting ||
                 roomBubbleTableViewCell.bubbleData.attachment.eventSentState == MXEventSentStateUploading)
        {
            // Offer to cancel the upload in progress
            // Upload id is stored in attachment url (nasty trick)
            NSString *uploadId = roomBubbleTableViewCell.bubbleData.attachment.contentURL;
            if ([MXMediaManager existingUploaderWithId:uploadId])
            {
                if (currentAlert)
                {
                    [currentAlert dismissViewControllerAnimated:NO completion:nil];
                }
                
                __weak __typeof(self) weakSelf = self;
                UIAlertController *cancelAlert = [UIAlertController alertControllerWithTitle:nil
                                                                                     message:[VectorL10n attachmentCancelUpload]
                                                                              preferredStyle:UIAlertControllerStyleAlert];
                
                [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n no]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                                                                   
                                                                   typeof(self) self = weakSelf;
                                                                   self->currentAlert = nil;
                                                                   
                                                               }]];
                
                [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n yes]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                                                                   
                                                                   // TODO cancel the attachment encryption if it is in progress.
                                                                   
                                                                   // Get again the loader
                                                                   MXMediaLoader *loader = [MXMediaManager existingUploaderWithId:uploadId];
                                                                   if (loader)
                                                                   {
                                                                       [loader cancel];
                                                                   }
                                                                   
                                                                   // Hide the progress animation
                                                                   roomBubbleTableViewCell.progressView.hidden = YES;
                                                                   
                                                                   if (weakSelf)
                                                                   {
                                                                       typeof(self) self = weakSelf;
                                                                       self->currentAlert = nil;
                                                                       
                                                                       // Remove the outgoing message and its related cached file.
                                                                       [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.cacheFilePath error:nil];
                                                                       [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.thumbnailCachePath error:nil];
                                                                       [self.roomDataSource removeEventWithEventId:roomBubbleTableViewCell.bubbleData.attachment.eventId];
                                                                   }
                                                                   
                                                               }]];
                
                [self presentViewController:cancelAlert animated:YES completion:nil];
                currentAlert = cancelAlert;
            }
        }
    }
    else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnEvent] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
    {
        [self dismissKeyboard];
        
        MXEvent *selectedEvent = userInfo[kMXKRoomBubbleCellEventKey];
        MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
        MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment;
        
        if (selectedEvent)
        {
            if (currentAlert)
            {
                [currentAlert dismissViewControllerAnimated:NO completion:nil];
                currentAlert = nil;
                
                // Cancel potential text selection in other bubbles
                for (MXKRoomBubbleTableViewCell *bubble in self.bubblesTableView.visibleCells)
                {
                    [bubble highlightTextMessageForEvent:nil];
                }
            }
            
            __weak __typeof(self) weakSelf = self;
            UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
            
            // Add actions for a failed event
            if (selectedEvent.sentState == MXEventSentStateFailed)
            {
                [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n resend]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                                                                   
                                                                   typeof(self) self = weakSelf;
                                                                   self->currentAlert = nil;
                                                                   
                                                                   // Let the datasource resend. It will manage local echo, etc.
                                                                   [self.roomDataSource resendEventWithEventId:selectedEvent.eventId success:nil failure:nil];
                                                                   
                                                               }]];
                
                [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n delete]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                                                                   
                                                                   typeof(self) self = weakSelf;
                                                                   self->currentAlert = nil;
                                                                   
                                                                   [self.roomDataSource removeEventWithEventId:selectedEvent.eventId];
                                                                   
                                                               }]];
            }
            
            // Add actions for text message
            if (!attachment)
            {
                // Highlight the select event
                [roomBubbleTableViewCell highlightTextMessageForEvent:selectedEvent.eventId];
                
                // Retrieved data related to the selected event
                NSArray *components = roomBubbleTableViewCell.bubbleData.bubbleComponents;
                MXKRoomBubbleComponent *selectedComponent;
                for (selectedComponent in components)
                {
                    if ([selectedComponent.event.eventId isEqualToString:selectedEvent.eventId])
                    {
                        break;
                    }
                    selectedComponent = nil;
                }
                
                [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n copy]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                                                                   
                                                                   typeof(self) self = weakSelf;
                                                                   self->currentAlert = nil;
                                                                   
                                                                   // Cancel event highlighting
                                                                   [roomBubbleTableViewCell highlightTextMessageForEvent:nil];
                                                                   
                                                                   NSString *textMessage = selectedComponent.textMessage;
                                                                   
                                                                   if (textMessage)
                                                                   {
                                                                       MXKPasteboardManager.shared.pasteboard.string = textMessage;
                                                                   }
                                                                   else
                                                                   {
                                                                       MXLogDebug(@"[MXKRoomViewController] Copy text failed. Text is nil for room id/event id: %@/%@", selectedComponent.event.roomId, selectedComponent.event.eventId);
                                                                   }
                                                               }]];
                
                if ([MXKAppSettings standardAppSettings].messageDetailsAllowSharing)
                {
                    [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n share]
                                                                    style:UIAlertActionStyleDefault
                                                                  handler:^(UIAlertAction * action) {
                        
                        typeof(self) self = weakSelf;
                        self->currentAlert = nil;
                        
                        // Cancel event highlighting
                        [roomBubbleTableViewCell highlightTextMessageForEvent:nil];
                        
                        NSArray *activityItems = [NSArray arrayWithObjects:selectedComponent.textMessage, nil];
                        
                        UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil];
                        if (activityViewController)
                        {
                            activityViewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
                            activityViewController.popoverPresentationController.sourceView = roomBubbleTableViewCell;
                            activityViewController.popoverPresentationController.sourceRect = roomBubbleTableViewCell.bounds;
                            
                            [self presentViewController:activityViewController animated:YES completion:nil];
                        }
                        
                    }]];
                }
                
                if (components.count > 1)
                {
                    [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n selectAll]
                                                                    style:UIAlertActionStyleDefault
                                                                  handler:^(UIAlertAction * action) {
                                                                       
                                                                       typeof(self) self = weakSelf;
                                                                       self->currentAlert = nil;
                                                                       
                                                                       [self selectAllTextMessageInCell:cell];
                                                                       
                                                                   }]];
                }
            }
            else // Add action for attachment
            {
                if (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo)
                {
                    if ([MXKAppSettings standardAppSettings].messageDetailsAllowSaving)
                    {
                        [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n save]
                                                                        style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {
                            
                            typeof(self) self = weakSelf;
                            self->currentAlert = nil;
                            
                            [self startActivityIndicator];
                            
                            [attachment save:^{
                                
                                typeof(self) self = weakSelf;
                                [self stopActivityIndicator];
                                
                            } failure:^(NSError *error) {
                                
                                typeof(self) self = weakSelf;
                                [self stopActivityIndicator];
                                
                                // Notify MatrixKit user
                                [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
                                
                            }];
                            
                            // Start animation in case of download during attachment preparing
                            [roomBubbleTableViewCell startProgressUI];
                            
                        }]];
                    }
                }
                
                if (attachment.type != MXKAttachmentTypeSticker)
                {
                    [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n copyButtonName]
                                                                    style:UIAlertActionStyleDefault
                                                                  handler:^(UIAlertAction * action) {
                                                                       
                                                                       typeof(self) self = weakSelf;
                                                                       self->currentAlert = nil;
                                                                       
                                                                       [self startActivityIndicator];
                                                                       
                                                                       [attachment copy:^{
                                                                           
                                                                           typeof(self) self = weakSelf;
                                                                           [self stopActivityIndicator];
                                                                           
                                                                       } failure:^(NSError *error) {
                                                                           
                                                                           typeof(self) self = weakSelf;
                                                                           [self stopActivityIndicator];
                                                                           
                                                                           // Notify MatrixKit user
                                                                           [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
                                                                           
                                                                       }];
                                                                       
                                                                       // Start animation in case of download during attachment preparing
                                                                       [roomBubbleTableViewCell startProgressUI];
                                                                       
                                                                   }]];
                    
                    if ([MXKAppSettings standardAppSettings].messageDetailsAllowSharing)
                    {
                        [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n share]
                                                                        style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {
                            
                            typeof(self) self = weakSelf;
                            self->currentAlert = nil;
                            
                            [attachment prepareShare:^(NSURL *fileURL) {
                                
                                typeof(self) self = weakSelf;
                                self->documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:fileURL];
                                [self->documentInteractionController setDelegate:self];
                                self->currentSharedAttachment = attachment;
                                
                                if (![self->documentInteractionController presentOptionsMenuFromRect:self.view.frame inView:self.view animated:YES])
                                {
                                    self->documentInteractionController = nil;
                                    [attachment onShareEnded];
                                    self->currentSharedAttachment = nil;
                                }
                                
                            } failure:^(NSError *error) {
                                
                                // Notify MatrixKit user
                                [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
                                
                            }];
                            
                            // Start animation in case of download during attachment preparing
                            [roomBubbleTableViewCell startProgressUI];
                            
                        }]];
                    }
                }
                
                // Check status of the selected event
                if (selectedEvent.sentState == MXEventSentStatePreparing ||
                    selectedEvent.sentState == MXEventSentStateEncrypting ||
                    selectedEvent.sentState == MXEventSentStateUploading)
                {
                    // Upload id is stored in attachment url (nasty trick)
                    NSString *uploadId = roomBubbleTableViewCell.bubbleData.attachment.contentURL;
                    if ([MXMediaManager existingUploaderWithId:uploadId])
                    {
                        [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n cancelUpload]
                                                                        style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {
                                                                           
                                                                           // TODO cancel the attachment encryption if it is in progress.
                                                                           
                                                                           // Cancel the loader
                                                                           MXMediaLoader *loader = [MXMediaManager existingUploaderWithId:uploadId];
                                                                           if (loader)
                                                                           {
                                                                               [loader cancel];
                                                                           }
                                                                           
                                                                           // Hide the progress animation
                                                                           roomBubbleTableViewCell.progressView.hidden = YES;
                                                                           
                                                                           if (weakSelf)
                                                                           {
                                                                               typeof(self) self = weakSelf;
                                                                               self->currentAlert = nil;
                                                                               
                                                                               // Remove the outgoing message and its related cached file.
                                                                               [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.cacheFilePath error:nil];
                                                                               [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.thumbnailCachePath error:nil];
                                                                               [self.roomDataSource removeEventWithEventId:selectedEvent.eventId];
                                                                           }
                                                                           
                                                                       }]];
                    }
                }
            }
            
            // Check status of the selected event
            if (selectedEvent.sentState == MXEventSentStateSent)
            {
                // Check whether download is in progress
                if (selectedEvent.isMediaAttachment)
                {
                    NSString *downloadId = roomBubbleTableViewCell.bubbleData.attachment.downloadId;
                    if ([MXMediaManager existingDownloaderWithIdentifier:downloadId])
                    {
                        [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n cancelDownload]
                                                                        style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {
                                                                           
                                                                           typeof(self) self = weakSelf;
                                                                           self->currentAlert = nil;
                                                                           
                                                                           // Get again the loader
                                                                           MXMediaLoader *loader = [MXMediaManager existingDownloaderWithIdentifier:downloadId];
                                                                           if (loader)
                                                                           {
                                                                               [loader cancel];
                                                                           }
                                                                           // Hide the progress animation
                                                                           roomBubbleTableViewCell.progressView.hidden = YES;
                                                                           
                                                                       }]];
                    }
                }
                
                [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n showDetails]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                                                                   
                                                                   typeof(self) self = weakSelf;
                                                                   self->currentAlert = nil;
                                                                   
                                                                   // Cancel event highlighting (if any)
                                                                   [roomBubbleTableViewCell highlightTextMessageForEvent:nil];
                                                                   
                                                                   // Display event details
                                                                   [self showEventDetails:selectedEvent];
                                                                   
                                                               }]];
            }
            
            [actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                            style:UIAlertActionStyleCancel
                                                          handler:^(UIAlertAction * action) {
                                                               
                                                               typeof(self) self = weakSelf;
                                                               self->currentAlert = nil;
                                                               
                                                               // Cancel event highlighting (if any)
                                                               [roomBubbleTableViewCell highlightTextMessageForEvent:nil];
                                                               
                                                           }]];
            
            // Do not display empty action sheet
            if (actionSheet.actions.count > 1)
            {
                [actionSheet popoverPresentationController].sourceView = roomBubbleTableViewCell;
                [actionSheet popoverPresentationController].sourceRect = roomBubbleTableViewCell.bounds;
                [self presentViewController:actionSheet animated:YES completion:nil];
                currentAlert = actionSheet;
            }
        }
    }
    else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnAvatarView])
    {
        MXLogDebug(@"    -> Avatar of %@ has been long pressed", userInfo[kMXKRoomBubbleCellUserIdKey]);
    }
    else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellUnsentButtonPressed])
    {
        MXEvent *selectedEvent = userInfo[kMXKRoomBubbleCellEventKey];
        if (selectedEvent)
        {
            // The user may want to resend it
            [self promptUserToResendEvent:selectedEvent.eventId];
        }
    }
}

#pragma mark - Clipboard

- (void)selectAllTextMessageInCell:(id<MXKCellRendering>)cell
{
    if (![MXKAppSettings standardAppSettings].messageDetailsAllowSharing)
    {
        return;
    }
    
    if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
    {
        MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
        selectedText = roomBubbleTableViewCell.bubbleData.textMessage;
        roomBubbleTableViewCell.allTextHighlighted = YES;
        
        // Display Menu (dispatch is required here, else the attributed text change hides the menu)
        dispatch_async(dispatch_get_main_queue(), ^{
            MXWeakify(self);
            self.uiMenuControllerDidHideMenuNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIMenuControllerDidHideMenuNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
                
                MXStrongifyAndReturnIfNil(self);
                // Deselect text
                roomBubbleTableViewCell.allTextHighlighted = NO;
                self->selectedText = nil;
                
                [UIMenuController sharedMenuController].menuItems = nil;
                
                [[NSNotificationCenter defaultCenter] removeObserver:self.uiMenuControllerDidHideMenuNotificationObserver];
            }];
            
            [self becomeFirstResponder];
            UIMenuController *menu = [UIMenuController sharedMenuController];
            menu.menuItems = @[[[UIMenuItem alloc] initWithTitle:[VectorL10n share] action:@selector(share:)]];
            [menu setTargetRect:roomBubbleTableViewCell.messageTextView.frame inView:roomBubbleTableViewCell];
            [menu setMenuVisible:YES animated:YES];
        });
    }
}

- (void)copy:(id)sender
{
    if (selectedText)
    {
        MXKPasteboardManager.shared.pasteboard.string = selectedText;
    }
    else
    {
        MXLogDebug(@"[MXKRoomViewController] Selected text copy failed. Selected text is nil");
    }
}

- (void)share:(id)sender
{
    if (selectedText)
    {
        NSArray *activityItems = [NSArray arrayWithObjects:selectedText, nil];
        
        UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil];
        if (activityViewController)
        {
            activityViewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
            activityViewController.popoverPresentationController.sourceView = self.view;
            activityViewController.popoverPresentationController.sourceRect = self.view.bounds;
            
            [self presentViewController:activityViewController animated:YES completion:nil];
        }
    }
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
    if (selectedText.length && (action == @selector(copy:) || action == @selector(share:)))
    {
        return YES;
    }
    return NO;
}

- (BOOL)canBecomeFirstResponder
{
    return (selectedText.length != 0);
}

#pragma mark - UITableView delegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (tableView == _bubblesTableView)
    {
        return [roomDataSource cellHeightAtIndex:indexPath.row withMaximumWidth:self.tableViewSafeAreaWidth];
    }
    
    return 0;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (tableView == _bubblesTableView)
    {
        // Dismiss keyboard when user taps on messages table view content
        [self dismissKeyboard];
    }
}

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
    // Release here resources, and restore reusable cells
    if ([cell respondsToSelector:@selector(didEndDisplay)])
    {
        [(id<MXKCellRendering>)cell didEndDisplay];
    }
}

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    // Detect vertical bounce at the top of the tableview to trigger pagination
    if (scrollView == _bubblesTableView)
    {
        // Detect top bounce
        if (scrollView.contentOffset.y < -scrollView.adjustedContentInset.top)
        {
            // Shall we add back pagination spinner?
            if (isPaginationInProgress && !backPaginationActivityView)
            {
                UIActivityIndicatorView* spinner  = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
                spinner.hidesWhenStopped = NO;
                spinner.backgroundColor = [UIColor clearColor];
                [spinner startAnimating];
                
                // no need to manage constraints here
                // IOS defines them.
                // since IOS7 the spinner is centered so need to create a background and add it.
                _bubblesTableView.tableHeaderView = backPaginationActivityView = spinner;
            }
        }
        else
        {
            // Shall we add forward pagination spinner?
            if (!roomDataSource.isLive && isPaginationInProgress && scrollView.contentOffset.y + scrollView.frame.size.height > scrollView.contentSize.height + 64 && !reconnectingView)
            {
                [self addReconnectingView];
            }
            else
            {
                [self detectPullToKick:scrollView];
            }
        }
    }
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (scrollView == _bubblesTableView)
    {
        // if the user scrolls the history content without animation
        // upateCurrentEventIdAtTableBottom must be called here (without dispatch).
        // else it will be done in scrollViewDidEndDecelerating
        if (!decelerate)
        {
            [self updateCurrentEventIdAtTableBottom:YES];
        }
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    if (scrollView == _bubblesTableView)
    {
        // do not dispatch the upateCurrentEventIdAtTableBottom call
        // else it might triggers weird UI lags.
        [self updateCurrentEventIdAtTableBottom:YES];
        [self managePullToKick:scrollView];
    }
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
    if (scrollView == _bubblesTableView)
    {
        // do not dispatch the upateCurrentEventIdAtTableBottom call
        // else it might triggers weird UI lags.
        [self updateCurrentEventIdAtTableBottom:YES];
    }
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (scrollView == _bubblesTableView)
    {
        BOOL wasScrollingToBottom = isScrollingToBottom;

        // Consider this callback to reset scrolling to bottom flag
        isScrollingToBottom = NO;
        
        // shouldScrollToBottomOnTableRefresh is used to inhibit false detection of
        // scrolling action from the user when the viewVC appears or rotates
        if (scrollView == _bubblesTableView && scrollView.contentSize.height && !shouldScrollToBottomOnTableRefresh)
        {
            // when the content size if smaller that the frame
            // scrollViewDidEndDecelerating is not called
            // so test it when the content offset goes back to the screen top.
            if ((scrollView.contentSize.height < scrollView.frame.size.height) && (-scrollView.contentOffset.y == scrollView.adjustedContentInset.top))
            {
                [self managePullToKick:scrollView];
            }
            
            // Trigger inconspicuous pagination when user scrolls toward the top
            if (scrollView.contentOffset.y < _paginationThreshold)
            {
                [self triggerPagination:_paginationLimit direction:MXTimelineDirectionBackwards];
            }
            // Enable forwards pagination when displaying non live timeline
            else if (!roomDataSource.isLive && !wasScrollingToBottom && ((scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.frame.size.height) < _paginationThreshold))
            {
                [self triggerPagination:_paginationLimit direction:MXTimelineDirectionForwards];
            }
        }
        
        if (wasScrollingToBottom)
        {
            // When scrolling to the bottom is performed without animation, 'scrollViewDidEndScrollingAnimation' is not called.
            // upateCurrentEventIdAtTableBottom must be called here (without dispatch).
            [self updateCurrentEventIdAtTableBottom:YES];
        }
    }
}

#pragma mark - MXKRoomTitleViewDelegate

- (void)roomTitleView:(MXKRoomTitleView*)titleView presentAlertController:(UIAlertController *)alertController
{
    [self dismissKeyboard];
    [self presentViewController:alertController animated:YES completion:nil];
}

- (BOOL)roomTitleViewShouldBeginEditing:(MXKRoomTitleView*)titleView
{
    return YES;
}

- (void)roomTitleView:(MXKRoomTitleView*)titleView isSaving:(BOOL)saving
{
    if (saving)
    {
        [self startActivityIndicator];
    }
    else
    {
        [self stopActivityIndicator];
    }
}

#pragma mark - MXKRoomInputToolbarViewDelegate

- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView hideStatusBar:(BOOL)isHidden
{
    isStatusBarHidden = isHidden;
    
    // Trigger status bar update
    [self setNeedsStatusBarAppearanceUpdate];
    
    // Handle status bar with the historical method.
    // TODO: remove this [UIApplication statusBarHidden] use (deprecated since iOS 9).
    // Note: setting statusBarHidden does nothing if your application is using the default UIViewController-based status bar system.
    UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
    if (sharedApplication)
    {
        sharedApplication.statusBarHidden = isHidden;
    }
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView isTyping:(BOOL)typing
{
    if (_saveProgressTextInput && roomDataSource)
    {
        // Store the potential message partially typed in text input
        roomDataSource.partialAttributedTextMessage = inputToolbarView.attributedTextMessage;
    }
    
    [self handleTypingState:typing];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView heightDidChanged:(CGFloat)height completion:(void (^)(BOOL finished))completion
{
    // This dispatch fixes a simultaneous accesses crash if this gets called twice quickly in succession
    dispatch_async(dispatch_get_main_queue(), ^{
        // Update layout with animation
        [UIView animateWithDuration:self.resizeComposerAnimationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
                         animations:^{
            // We will scroll to bottom if the bottom of the table is currently visible
            BOOL shouldScrollToBottom = [self isBubblesTableScrollViewAtTheBottom];

            self->_roomInputToolbarContainerHeightConstraint.constant = height;
            CGFloat bubblesTableViewBottomConst = self->_roomInputToolbarContainerBottomConstraint.constant + self->_roomInputToolbarContainerHeightConstraint.constant + self->_roomActivitiesContainerHeightConstraint.constant;

            self->_bubblesTableViewBottomConstraint.constant = bubblesTableViewBottomConst;

            // Force to render the view
            [self.view layoutIfNeeded];
            
            if (shouldScrollToBottom)
            {
                [self scrollBubblesTableViewToBottomAnimated:NO];
            }
        }
                         completion:^(BOOL finished){
            if (completion)
            {
                completion(finished);
            }
        }];
    });
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendTextMessage:(NSString*)textMessage
{
    // Handle potential IRC commands in typed string
    if ([self sendAsIRCStyleCommandIfPossible:textMessage] == NO)
    {
        // Send text message in the current room
        [self sendTextMessage:textMessage];
    }
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendImage:(UIImage*)image
{
    // Let the datasource send it and manage the local echo
    [roomDataSource sendImage:image success:nil failure:^(NSError *error)
    {
        // Nothing to do. The image is marked as unsent in the room history by the datasource
        MXLogDebug(@"[MXKRoomViewController] sendImage failed.");
    }];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendImage:(NSData*)imageData withMimeType:(NSString*)mimetype
{
    // Let the datasource send it and manage the local echo
    [roomDataSource sendImage:imageData mimeType:mimetype success:nil failure:^(NSError *error)
    {
        // Nothing to do. The image is marked as unsent in the room history by the datasource
        MXLogDebug(@"[MXKRoomViewController] sendImage with mimetype failed.");
    }];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendVideo:(NSURL*)videoLocalURL withThumbnail:(UIImage*)videoThumbnail
{
    AVURLAsset *videoAsset = [AVURLAsset assetWithURL:videoLocalURL];
    [self roomInputToolbarView:toolbarView sendVideoAsset:videoAsset withThumbnail:videoThumbnail];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendVideoAsset:(AVAsset*)videoAsset withThumbnail:(UIImage*)videoThumbnail
{
    // Let the datasource send it and manage the local echo
    [roomDataSource sendVideoAsset:videoAsset withThumbnail:videoThumbnail success:nil failure:^(NSError *error)
    {
        // Nothing to do. The video is marked as unsent in the room history by the datasource
        MXLogDebug(@"[MXKRoomViewController] sendVideo failed.");
    }];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView sendFile:(NSURL *)fileLocalURL withMimeType:(NSString*)mimetype
{
    // Let the datasource send it and manage the local echo
    [roomDataSource sendFile:fileLocalURL mimeType:mimetype success:nil failure:^(NSError *error)
     {
         // Nothing to do. The file is marked as unsent in the room history by the datasource
         MXLogDebug(@"[MXKRoomViewController] sendFile failed.");
     }];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView presentAlertController:(UIAlertController *)alertController
{
    [self dismissKeyboard];
    [self presentViewController:alertController animated:YES completion:nil];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView presentViewController:(UIViewController*)viewControllerToPresent
{
    [self dismissKeyboard];
    [self presentViewController:viewControllerToPresent animated:YES completion:nil];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
{
    [self dismissViewControllerAnimated:flag completion:completion];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView updateActivityIndicator:(BOOL)isAnimating
{
    isInputToolbarProcessing = isAnimating;
    
    if (isAnimating)
    {
        [self startActivityIndicator];
    }
    else
    {
        [self stopActivityIndicator];
    }
}
# pragma mark - Typing notification

- (void)handleTypingState:(BOOL)typing
{
    NSUInteger notificationTimeoutMS = -1;
    if (typing)
    {
        // Check whether a typing event has been already reported to server (We wait for the end of the local timout before considering this new event)
        if (typingTimer)
        {
            // Refresh date of the last observed typing
            lastTypingDate = [[NSDate alloc] init];
            return;
        }
        
        // No typing event has been yet reported -> share encryption keys if requested
        if ([MXKAppSettings standardAppSettings].outboundGroupSessionKeyPreSharingStrategy == MXKKeyPreSharingWhenTyping)
        {
            [self shareEncryptionKeys];
        }
        
        // Launch a timer to prevent sending multiple typing notifications
        NSTimeInterval timerTimeout = MXKROOMVIEWCONTROLLER_DEFAULT_TYPING_TIMEOUT_SEC;
        if (lastTypingDate)
        {
            NSTimeInterval lastTypingAge = -[lastTypingDate timeIntervalSinceNow];
            if (lastTypingAge < timerTimeout)
            {
                // Subtract the time interval since last typing from the timer timeout
                timerTimeout -= lastTypingAge;
            }
            else
            {
                timerTimeout = 0;
            }
        }
        else
        {
            // Keep date of this typing event
            lastTypingDate = [[NSDate alloc] init];
        }
        
        if (timerTimeout)
        {
            typingTimer = [NSTimer scheduledTimerWithTimeInterval:timerTimeout target:self selector:@selector(typingTimeout:) userInfo:self repeats:NO];
            // Compute the notification timeout in ms (consider the double of the local typing timeout)
            notificationTimeoutMS = 2000 * MXKROOMVIEWCONTROLLER_DEFAULT_TYPING_TIMEOUT_SEC;
        }
        else
        {
            // This typing event is too old, we will ignore it
            typing = NO;
            MXLogDebug(@"[MXKRoomVC] Ignore typing event (too old)");
        }
    }
    else
    {
        // Cancel any typing timer
        [typingTimer invalidate];
        typingTimer = nil;
        // Reset last typing date
        lastTypingDate = nil;
    }
    
    [self sendTypingNotification:typing timeout:notificationTimeoutMS];
}

- (void)sendTypingNotification:(BOOL)typing timeout:(NSUInteger)notificationTimeoutMS
{
    MXWeakify(self);
    
    // Send typing notification to server
    [roomDataSource.room sendTypingNotification:typing
                                        timeout:notificationTimeoutMS
                                        success:^{
        
        MXStrongifyAndReturnIfNil(self);
                                            // Reset last typing date
                                            self->lastTypingDate = nil;
                                        } failure:^(NSError *error)
    {
        MXStrongifyAndReturnIfNil(self);
        
        MXLogDebug(@"[MXKRoomVC] Failed to send typing notification (%d)", typing);
        
        // Cancel timer (if any)
        [self->typingTimer invalidate];
        self->typingTimer = nil;
    }];
}

- (IBAction)typingTimeout:(id)sender
{
    [typingTimer invalidate];
    typingTimer = nil;
    
    // Check whether a new typing event has been observed
    BOOL typing = (lastTypingDate != nil);
    // Post a new typing notification
    [self handleTypingState:typing];
}


# pragma mark - Attachment handling

- (void)showAttachmentInCell:(UITableViewCell*)cell
{
    [self dismissKeyboard];

    // Retrieve the attachment information from the associated cell data
    if ([cell isKindOfClass:MXKTableViewCell.class])
    {
        MXKCellData *cellData = ((MXKTableViewCell*)cell).mxkCellData;

        // Only 'MXKRoomBubbleCellData' is supported here for the moment.
        if ([cellData isKindOfClass:MXKRoomBubbleCellData.class])
        {
            MXKRoomBubbleCellData *bubbleData = (MXKRoomBubbleCellData*)cellData;
            
            MXKAttachment *selectedAttachment = bubbleData.attachment;
            
            if (bubbleData.isAttachmentWithThumbnail)
            {
                // The attachments viewer is opened only on a valid attachment. It does not display the stickers.
                if (selectedAttachment.eventSentState == MXEventSentStateSent && selectedAttachment.type != MXKAttachmentTypeSticker)
                {
                    // Note: the stickers are presently excluded from the attachments list returned by the room dataSource.
                    NSArray *attachmentsWithThumbnail = self.roomDataSource.attachmentsWithThumbnail;
                    
                    MXKAttachmentsViewController *attachmentsViewer;
                    
                    // Present an attachment viewer
                    if (attachmentsViewerClass)
                    {
                        attachmentsViewer = [attachmentsViewerClass animatedAttachmentsViewControllerWithSourceViewController:self];
                    }
                    else
                    {
                        attachmentsViewer = [MXKAttachmentsViewController animatedAttachmentsViewControllerWithSourceViewController:self];
                    }
                    
                    attachmentsViewer.delegate = self;
                    attachmentsViewer.complete = ([roomDataSource.timeline canPaginate:MXTimelineDirectionBackwards] == NO);
                    attachmentsViewer.hidesBottomBarWhenPushed = YES;
                    [attachmentsViewer displayAttachments:attachmentsWithThumbnail focusOn:selectedAttachment.eventId];
                    
                    // Keep here the image view used to display the attachment in the selected cell.
                    // Note: Only `MXKRoomBubbleTableViewCell` and `MXKSearchTableViewCell` are supported for the moment.
                    if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
                    {
                        self.openedAttachmentImageView = ((MXKRoomBubbleTableViewCell *)cell).attachmentView.imageView;
                    }
                    else if ([cell isKindOfClass:MXKSearchTableViewCell.class])
                    {
                        self.openedAttachmentImageView = ((MXKSearchTableViewCell *)cell).attachmentImageView.imageView;
                    }
                    
                    self.openedAttachmentEventId = selectedAttachment.eventId;
                    
                    // "Initializing" closedAttachmentEventId so it is equal to openedAttachmentEventId at the beginning
                    self.closedAttachmentEventId = self.openedAttachmentEventId;
                    
                    if (@available(iOS 13.0, *))
                    {
                        attachmentsViewer.modalPresentationStyle = UIModalPresentationFullScreen;
                    }
                    
                    [self presentViewController:attachmentsViewer animated:YES completion:nil];
                    
                    self.attachmentsViewer = attachmentsViewer;
                }
                else
                {
                    // Let's the application do something
                    MXLogDebug(@"[MXKRoomVC] showAttachmentInCell on an unsent media");
                }
            }
            else if (selectedAttachment.type == MXKAttachmentTypeFile || selectedAttachment.type == MXKAttachmentTypeAudio)
            {
                // Start activity indicator as feedback on file selection.
                [self startActivityIndicator];
                
                [selectedAttachment prepareShare:^(NSURL *fileURL) {
                    
                    [self stopActivityIndicator];

                    MXWeakify(self);
                    void(^viewAttachment)(void) = ^() {
                        
                        MXStrongifyAndReturnIfNil(self);
                        
                        if (![self canPreviewFileAttachment:selectedAttachment withLocalFileURL:fileURL])
                        {
                            // When we don't support showing a preview for a file, show a share
                            // sheet if allowed, otherwise display an error to inform the user.
                            if (self.allowActionsInDocumentPreview)
                            {
                                UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[fileURL]
                                                                                                         applicationActivities:nil];
                                MXWeakify(self);
                                shareSheet.completionWithItemsHandler = ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
                                    MXStrongifyAndReturnIfNil(self);
                                    [selectedAttachment onShareEnded];
                                    self->currentSharedAttachment = nil;
                                };
                                
                                self->currentSharedAttachment = selectedAttachment;
                                [self presentViewController:shareSheet animated:YES completion:nil];
                            }
                            else
                            {
                                UIAlertController *alert = [UIAlertController alertControllerWithTitle:VectorL10n.attachmentUnsupportedPreviewTitle
                                                                                               message:VectorL10n.attachmentUnsupportedPreviewMessage
                                                                                        preferredStyle:UIAlertControllerStyleAlert];
                                MXWeakify(self);
                                [alert addAction:[UIAlertAction actionWithTitle:VectorL10n.ok style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
                                    MXStrongifyAndReturnIfNil(self);
                                    [selectedAttachment onShareEnded];
                                    self->currentAlert = nil;
                                }]];

                                [self presentViewController:alert animated:YES completion:nil];
                                self->currentAlert = alert;
                            }
                            
                            return;
                        }

                        if (self.allowActionsInDocumentPreview)
                        {
                            // We could get rid of this part of code and use only a MXKPreviewViewController
                            // Nevertheless, MXKRoomViewController is compliant to UIDocumentInteractionControllerDelegate
                            // and remove all this code could have effect on some custom implementations.
                            self->documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:fileURL];
                            [self->documentInteractionController setDelegate:self];
                            self->currentSharedAttachment = selectedAttachment;

                            if (![self->documentInteractionController presentPreviewAnimated:YES])
                            {
                                if (![self->documentInteractionController presentOptionsMenuFromRect:self.view.frame inView:self.view animated:YES])
                                {
                                    self->documentInteractionController = nil;
                                    [selectedAttachment onShareEnded];
                                    self->currentSharedAttachment = nil;
                                }
                            }
                        }
                        else
                        {
                            self->currentSharedAttachment = selectedAttachment;
                            [MXKPreviewViewController presentFrom:self fileUrl:fileURL allowActions:self.allowActionsInDocumentPreview delegate:self];
                        }
                    };

                    if (self->roomDataSource.mxSession.crypto
                        && [selectedAttachment.contentInfo[@"mimetype"] isEqualToString:@"text/plain"]
                        && [MXMegolmExportEncryption isMegolmKeyFile:fileURL])
                    {
                        // The file is a megolm key file
                        // Ask the user if they wants to view the file as a classic file attachment
                        // or open an import process
                        [self->currentAlert dismissViewControllerAnimated:NO completion:nil];

                        __weak typeof(self) weakSelf = self;
                        UIAlertController *keysPrompt = [UIAlertController alertControllerWithTitle:@""
                                                                                            message:[VectorL10n attachmentE2eKeysFilePrompt]
                                                                                     preferredStyle:UIAlertControllerStyleAlert];
                        
                        [keysPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n view]
                                                                       style:UIAlertActionStyleDefault
                                                                     handler:^(UIAlertAction * action) {
                                                                           
                                                                           // View file content
                                                                           if (weakSelf)
                                                                           {
                                                                               typeof(self) self = weakSelf;
                                                                               self->currentAlert = nil;
                                                                               
                                                                               viewAttachment();
                                                                           }
                                                                           
                                                                       }]];
                        
                        [keysPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n attachmentE2eKeysImport]
                                                                       style:UIAlertActionStyleDefault
                                                                     handler:^(UIAlertAction * action) {
                                                                           
                                                                           if (weakSelf)
                                                                           {
                                                                               typeof(self) self = weakSelf;
                                                                               self->currentAlert = nil;
                                                                               
                                                                               // Show the keys import dialog
                                                                               self->importView = [[MXKEncryptionKeysImportView alloc] initWithMatrixSession:self->roomDataSource.mxSession];
                                                                               self->currentAlert = self->importView.alertController;
                                                                               [self->importView showInViewController:self toImportKeys:fileURL onComplete:^{
                                                                                   
                                                                                   if (weakSelf)
                                                                                   {
                                                                                       typeof(self) self = weakSelf;
                                                                                       self->currentAlert = nil;
                                                                                       self->importView = nil;
                                                                                   }
                                                                                   
                                                                               }];
                                                                           }
                                                                           
                                                                       }]];
                        
                        [self presentViewController:keysPrompt animated:YES completion:nil];
                        self->currentAlert = keysPrompt;
                    }
                    else
                    {
                        viewAttachment();
                    }

                } failure:^(NSError *error) {
                    
                    [self stopActivityIndicator];
                    
                    // Notify MatrixKit user
                    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error];
                    
                }];
                
                if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
                {
                    // Start animation in case of download
                    MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
                    [roomBubbleTableViewCell startProgressUI];
                }
            }
        }
    }
}

- (BOOL)canPreviewFileAttachment:(MXKAttachment *)attachment withLocalFileURL:(NSURL *)localFileURL
{
    // Sanity check.
    if (![NSFileManager.defaultManager isReadableFileAtPath:localFileURL.path])
    {
        return NO;
    }
    
    if (UIDevice.currentDevice.systemVersion.floatValue >= 13)
    {
        return YES;
    }
    
    MXKUTI *attachmentUTI = attachment.uti;
    MXKUTI *fileUTI = [[MXKUTI alloc] initWithLocalFileURL:localFileURL];
    if (!attachmentUTI || !fileUTI)
    {
        return NO;
    }
    
    NSArray<MXKUTI *> *unsupportedUTIs = @[MXKUTI.html, MXKUTI.xml, MXKUTI.svg];
    if ([attachmentUTI conformsToAnyOf:unsupportedUTIs] || [fileUTI conformsToAnyOf:unsupportedUTIs])
    {
        return NO;
    }
    
    return YES;
}

#pragma mark - MXKAttachmentsViewControllerDelegate

- (BOOL)attachmentsViewController:(MXKAttachmentsViewController*)attachmentsViewController paginateAttachmentBefore:(NSString*)eventId
{
    [self triggerAttachmentBackPagination:eventId];
    
    return [self.roomDataSource.timeline canPaginate:MXTimelineDirectionBackwards];
}

- (void)displayedNewAttachmentWithEventId:(NSString *)eventId {
    self.closedAttachmentEventId = eventId;
}

#pragma mark - MXKRoomActivitiesViewDelegate

- (void)didChangeHeight:(MXKRoomActivitiesView *)roomActivitiesView oldHeight:(CGFloat)oldHeight newHeight:(CGFloat)newHeight
{
    // We will scroll to bottom if the bottom of the table is currently visible
    BOOL shouldScrollToBottom = [self isBubblesTableScrollViewAtTheBottom];

    // Apply height change to constraints 
    _roomActivitiesContainerHeightConstraint.constant = newHeight;
    _bubblesTableViewBottomConstraint.constant += newHeight - oldHeight;

    // Force to render the view
    [self.view layoutIfNeeded];

    if (shouldScrollToBottom)
    {
        [self scrollBubblesTableViewToBottomAnimated:YES];
    }
}

#pragma mark - MXKPreviewViewControllerDelegate

- (void)previewViewControllerDidEndPreview:(MXKPreviewViewController *)controller
{
    if (currentSharedAttachment)
    {
        [currentSharedAttachment onShareEnded];
        currentSharedAttachment = nil;
    }
}

#pragma mark - UIDocumentInteractionControllerDelegate

- (UIViewController *)documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *) controller
{
    return self;
}

// Preview presented/dismissed on document.  Use to set up any HI underneath.
- (void)documentInteractionControllerWillBeginPreview:(UIDocumentInteractionController *)controller
{
    documentInteractionController = controller;
}

- (void)documentInteractionControllerDidEndPreview:(UIDocumentInteractionController *)controller
{
    documentInteractionController = nil;
    if (currentSharedAttachment)
    {
        [currentSharedAttachment onShareEnded];
        currentSharedAttachment = nil;
    }
}

- (void)documentInteractionControllerDidDismissOptionsMenu:(UIDocumentInteractionController *)controller
{
    documentInteractionController = nil;
    if (currentSharedAttachment)
    {
        [currentSharedAttachment onShareEnded];
        currentSharedAttachment = nil;
    }
}

- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller
{
    documentInteractionController = nil;
    if (currentSharedAttachment)
    {
        [currentSharedAttachment onShareEnded];
        currentSharedAttachment = nil;
    }
}

#pragma mark - resync management

- (void)onSyncNotification
{
    latestServerSync = [NSDate date];
    [self removeReconnectingView];
}

- (BOOL)canReconnect
{
    // avoid restarting connection if some data has been received within 1 second (1000 : latestServerSync is null)
    NSTimeInterval interval = latestServerSync ? [[NSDate date] timeIntervalSinceDate:latestServerSync] : 1000;
    return  (interval > 1) && [self.mainSession reconnect];
}

- (void)addReconnectingView
{
    if (!reconnectingView)
    {
        UIActivityIndicatorView* spinner  = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        [spinner sizeToFit];
        spinner.hidesWhenStopped = NO;
        spinner.backgroundColor = [UIColor clearColor];
        [spinner startAnimating];
        
        // no need to manage constraints here
        // IOS defines them.
        // since IOS7 the spinner is centered so need to create a background and add it.
        _bubblesTableView.tableFooterView = reconnectingView = spinner;
    }
}

- (void)removeReconnectingView
{
    if (reconnectingView && !restartConnection)
    {
        _bubblesTableView.tableFooterView = reconnectingView = nil;
    }
}

/**
 Detect if the current connection must be restarted.
 The spinner is displayed until the overscroll ends (and scrollViewDidEndDecelerating is called).
 */
- (void)detectPullToKick:(UIScrollView *)scrollView
{
    if (roomDataSource.isLive && !reconnectingView)
    {
        // detect if the user scrolls over the tableview bottom
        restartConnection = (
                             ((scrollView.contentSize.height < scrollView.frame.size.height) && (scrollView.contentOffset.y > 128))
                             ||
                             ((scrollView.contentSize.height > scrollView.frame.size.height) &&  (scrollView.contentOffset.y + scrollView.frame.size.height) > (scrollView.contentSize.height + 128)));
        
        if (restartConnection)
        {
            // wait that list decelerate to display / hide it
            [self addReconnectingView];
        }
    }
}


/**
 Restarts the current connection if it is required.
 The 0.3s delay is added to avoid flickering if the connection does not require to be restarted.
 */
- (void)managePullToKick:(UIScrollView *)scrollView
{
    // the current connection must be restarted
    if (roomDataSource.isLive && restartConnection)
    {
        // display at least 0.3s the spinner to show to the user that something is pending
        // else the UI is flickering
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            self->restartConnection = NO;
            
            if (![self canReconnect])
            {
                // if the event stream has not been restarted
                // hide the spinner
                [self removeReconnectingView];
            }
            // else wait that onSyncNotification is called.
        });
    }
}

#pragma mark - MXKSourceAttachmentAnimatorDelegate

- (UIImageView *)originalImageView {
    if ([self.openedAttachmentEventId isEqualToString:self.closedAttachmentEventId]) {
        return self.openedAttachmentImageView;
    }
    return nil;
}


- (CGRect)convertedFrameForOriginalImageView {
    if ([self.openedAttachmentEventId isEqualToString:self.closedAttachmentEventId]) {
        return [self.openedAttachmentImageView convertRect:self.openedAttachmentImageView.frame toView:nil];
    }
    //default frame which will be used if the user scrolls to other attachments in MXKAttachmentsViewController
    return CGRectMake(CGRectGetWidth(self.view.frame)/2, 0.0, 0.0, 0.0);
}

#pragma mark - Encryption key sharing

- (void)shareEncryptionKeys
{
    __block NSString *roomId = roomDataSource.roomId;
    [roomDataSource.mxSession.crypto ensureEncryptionInRoom:roomId success:^{
        MXLogDebug(@"[MXKRoomViewController] Key shared for room: %@", roomId);
    } failure:^(NSError *error) {
        MXLogDebug(@"[MXKRoomViewController] Failed to share key for room %@: %@", roomId, error);
    }];
}

@end
